import { isUserCreateParamsPOST, isUserSetActiveStatusParamsPOST, isUserDeactivateIdleParamsPOST, isUsersInfoParamsGetProps, isUserRegisterParamsPOST, isUserLogoutParamsPOST, isUsersListTeamsProps, isUsersAutocompleteProps, isUsersSetAvatarProps, isUsersUpdateParamsPOST, isUsersUpdateOwnBasicInfoParamsPOST, isUsersSetPreferencesParamsPOST, isUsersCheckUsernameAvailabilityParamsGET, isUsersSendConfirmationEmailParamsPOST, } from '@rocket.chat/rest-typings'; import { Meteor } from 'meteor/meteor'; import { Accounts } from 'meteor/accounts-base'; import { Match, check } from 'meteor/check'; import type { IExportOperation, ILoginToken, IPersonalAccessToken, IUser, UserStatus } from '@rocket.chat/core-typings'; import { Users, Subscriptions } from '@rocket.chat/models'; import type { Filter } from 'mongodb'; import { Team, api } from '@rocket.chat/core-services'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; import { settings } from '../../../settings/server'; import { validateCustomFields, saveUser, saveCustomFieldsWithoutValidation, setUserAvatar, saveCustomFields } from '../../../lib/server'; import { checkUsernameAvailability, checkUsernameAvailabilityWithValidation, } from '../../../lib/server/functions/checkUsernameAvailability'; import { getFullUserDataByIdOrUsername } from '../../../lib/server/functions/getFullUserData'; import { setStatusText } from '../../../lib/server/functions/setStatusText'; import { API } from '../api'; import { findUsersToAutocomplete, getInclusiveFields, getNonEmptyFields, getNonEmptyQuery } from '../lib/users'; import { getUserForCheck, emailCheck } from '../../../2fa/server/code'; import { resetUserE2EEncriptionKey } from '../../../../server/lib/resetUserE2EKey'; import { resetTOTP } from '../../../2fa/server/functions/resetTOTP'; import { isValidQuery } from '../lib/isValidQuery'; import { getURL } from '../../../utils/server'; import { getUploadFormData } from '../lib/getUploadFormData'; import { getPaginationItems } from '../helpers/getPaginationItems'; import { getUserFromParams } from '../helpers/getUserFromParams'; import { isUserFromParams } from '../helpers/isUserFromParams'; import { saveUserPreferences } from '../../../../server/methods/saveUserPreferences'; import { setUsernameWithValidation } from '../../../lib/server/functions/setUsername'; import { i18n } from '../../../../server/lib/i18n'; API.v1.addRoute( 'users.getAvatar', { authRequired: false }, { async get() { const user = await getUserFromParams(this.queryParams); const url = getURL(`/avatar/${user.username}`, { cdn: false, full: true }); this.response.setHeader('Location', url); return { statusCode: 307, body: url, }; }, }, ); API.v1.addRoute( 'users.getAvatarSuggestion', { authRequired: true, }, { async get() { const suggestions = await Meteor.callAsync('getAvatarSuggestion'); return API.v1.success({ suggestions }); }, }, ); API.v1.addRoute( 'users.update', { authRequired: true, twoFactorRequired: true, validateParams: isUsersUpdateParamsPOST }, { async post() { const userData = { _id: this.bodyParams.userId, ...this.bodyParams.data }; await saveUser(this.userId, userData); if (this.bodyParams.data.customFields) { await saveCustomFields(this.bodyParams.userId, this.bodyParams.data.customFields); } if (typeof this.bodyParams.data.active !== 'undefined') { const { userId, data: { active }, confirmRelinquish, } = this.bodyParams; await Meteor.callAsync('setUserActiveStatus', userId, active, Boolean(confirmRelinquish)); } const { fields } = await this.parseJsonQuery(); const user = await Users.findOneById(this.bodyParams.userId, { projection: fields }); if (!user) { return API.v1.failure('User not found'); } return API.v1.success({ user }); }, }, ); API.v1.addRoute( 'users.updateOwnBasicInfo', { authRequired: true, validateParams: isUsersUpdateOwnBasicInfoParamsPOST }, { async 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', }; await Meteor.callAsync('saveUserProfile', userData, this.bodyParams.customFields, twoFactorOptions); return API.v1.success({ user: await Users.findOneById(this.userId, { projection: API.v1.defaultFieldsToExclude }), }); }, }, ); API.v1.addRoute( 'users.setPreferences', { authRequired: true, validateParams: isUsersSetPreferencesParamsPOST }, { async post() { if ( this.bodyParams.userId && this.bodyParams.userId !== this.userId && !(await hasPermissionAsync(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 (!(await Users.findOneById(userId))) { throw new Meteor.Error('error-invalid-user', 'The optional "userId" param provided does not match any users'); } await saveUserPreferences(this.bodyParams.data, userId); const user = await Users.findOneById(userId, { projection: { 'settings.preferences': 1, 'language': 1, }, }); if (!user) { return API.v1.failure('User not found'); } return API.v1.success({ user: { _id: user._id, settings: { preferences: { ...user.settings?.preferences, language: user.language, }, }, } as unknown as Required>, }); }, }, ); API.v1.addRoute( 'users.setAvatar', { authRequired: true, validateParams: isUsersSetAvatarProps }, { async post() { const canEditOtherUserAvatar = await hasPermissionAsync(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 = await (async (): Promise< Pick | undefined | null > => { if (isUserFromParams(this.bodyParams, this.userId, this.user)) { return Users.findOneById(this.userId); } if (canEditOtherUserAvatar) { return getUserFromParams(this.bodyParams); } })(); if (!user) { return API.v1.unauthorized(); } if (this.bodyParams.avatarUrl) { await setUserAvatar(user, this.bodyParams.avatarUrl, '', 'url'); return API.v1.success(); } const image = await getUploadFormData( { request: this.request, }, { field: 'image', sizeLimit: settings.get('FileUpload_MaxFileSize') }, ); if (!image) { return API.v1.failure("The 'image' param is required"); } const { fields, fileBuffer, mimetype } = image; const sentTheUserByFormData = fields.userId || fields.username; if (sentTheUserByFormData) { if (fields.userId) { user = await Users.findOneById(fields.userId, { projection: { username: 1 } }); } else if (fields.username) { user = await Users.findOneByUsernameIgnoringCase(fields.username, { projection: { 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 && !(await hasPermissionAsync(this.userId, 'edit-other-user-avatar'))) { throw new Meteor.Error('error-not-allowed', 'Not allowed'); } } await setUserAvatar(user, fileBuffer, mimetype, 'rest'); return API.v1.success(); }, }, ); API.v1.addRoute( 'users.create', { authRequired: true, validateParams: isUserCreateParamsPOST }, { async post() { // New change made by pull request #5152 if (typeof this.bodyParams.joinDefaultChannels === 'undefined') { this.bodyParams.joinDefaultChannels = true; } if (this.bodyParams.customFields) { validateCustomFields(this.bodyParams.customFields); } const newUserId = await saveUser(this.userId, this.bodyParams); const userId = typeof newUserId !== 'string' ? this.userId : newUserId; if (this.bodyParams.customFields) { await saveCustomFieldsWithoutValidation(userId, this.bodyParams.customFields); } if (typeof this.bodyParams.active !== 'undefined') { await Meteor.callAsync('setUserActiveStatus', userId, this.bodyParams.active); } const { fields } = await this.parseJsonQuery(); const user = await Users.findOneById(userId, { projection: fields }); if (!user) { return API.v1.failure('User not found'); } return API.v1.success({ user }); }, }, ); API.v1.addRoute( 'users.delete', { authRequired: true }, { async post() { if (!(await hasPermissionAsync(this.userId, 'delete-user'))) { return API.v1.unauthorized(); } const user = await getUserFromParams(this.bodyParams); const { confirmRelinquish = false } = this.bodyParams; await Meteor.callAsync('deleteUser', user._id, confirmRelinquish); return API.v1.success(); }, }, ); API.v1.addRoute( 'users.deleteOwnAccount', { authRequired: true }, { async post() { const { password } = this.bodyParams; if (!password) { return API.v1.failure('Body parameter "password" is required.'); } if (!settings.get('Accounts_AllowDeleteOwnAccount')) { throw new Meteor.Error('error-not-allowed', 'Not allowed'); } const { confirmRelinquish = false } = this.bodyParams; await Meteor.callAsync('deleteUserOwnAccount', password, confirmRelinquish); return API.v1.success(); }, }, ); API.v1.addRoute( 'users.setActiveStatus', { authRequired: true, validateParams: isUserSetActiveStatusParamsPOST }, { async post() { if ( !(await hasPermissionAsync(this.userId, 'edit-other-user-active-status')) && !(await hasPermissionAsync(this.userId, 'manage-moderation-actions')) ) { return API.v1.unauthorized(); } const { userId, activeStatus, confirmRelinquish = false } = this.bodyParams; await Meteor.callAsync('setUserActiveStatus', userId, activeStatus, confirmRelinquish); const user = await Users.findOneById(this.bodyParams.userId, { projection: { active: 1 } }); if (!user) { return API.v1.failure('User not found'); } return API.v1.success({ user, }); }, }, ); API.v1.addRoute( 'users.deactivateIdle', { authRequired: true, validateParams: isUserDeactivateIdleParamsPOST }, { async post() { if (!(await hasPermissionAsync(this.userId, 'edit-other-user-active-status'))) { return API.v1.unauthorized(); } const { daysIdle, role = 'user' } = this.bodyParams; const lastLoggedIn = new Date(); lastLoggedIn.setDate(lastLoggedIn.getDate() - daysIdle); const count = (await Users.setActiveNotLoggedInAfterWithRole(lastLoggedIn, role, false)).modifiedCount; return API.v1.success({ count, }); }, }, ); API.v1.addRoute( 'users.info', { authRequired: true, validateParams: isUsersInfoParamsGetProps }, { async get() { const { fields } = await this.parseJsonQuery(); 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 || (await hasPermissionAsync(this.userId, 'view-other-user-channels')))) { return API.v1.success({ user: { ...user, rooms: await Subscriptions.findByUserId(user._id, { projection: { rid: 1, name: 1, t: 1, roles: 1, unread: 1, federated: 1, }, sort: { t: 1, name: 1, }, }).toArray(), }, }); } return API.v1.success({ user, }); }, }, ); API.v1.addRoute( 'users.list', { authRequired: true, queryOperations: ['$or', '$and'], }, { async get() { if (!(await hasPermissionAsync(this.userId, 'view-d-room'))) { return API.v1.unauthorized(); } if ( settings.get('API_Apply_permission_view-outside-room_on_users-list') && !(await hasPermissionAsync(this.userId, 'view-outside-room')) ) { return API.v1.unauthorized(); } const { offset, count } = await getPaginationItems(this.queryParams); const { sort, fields, query } = await this.parseJsonQuery(); const nonEmptyQuery = getNonEmptyQuery(query, await hasPermissionAsync(this.userId, 'view-full-other-user-info')); const nonEmptyFields = getNonEmptyFields(fields); const inclusiveFields = getInclusiveFields(nonEmptyFields); const inclusiveFieldsKeys = Object.keys(inclusiveFields); if ( !isValidQuery( nonEmptyQuery, [ ...inclusiveFieldsKeys, inclusiveFieldsKeys.includes('emails') && 'emails.address.*', inclusiveFieldsKeys.includes('username') && 'username.*', inclusiveFieldsKeys.includes('name') && 'name.*', inclusiveFieldsKeys.includes('type') && 'type.*', inclusiveFieldsKeys.includes('customFields') && 'customFields.*', ].filter(Boolean) as string[], this.queryOperations, ) ) { throw new Meteor.Error('error-invalid-query', isValidQuery.errors.join('\n')); } const actualSort = sort || { username: 1 }; if (sort?.status) { actualSort.active = sort.status; } if (sort?.name) { actualSort.nameInsensitive = sort.name; } const limit = count !== 0 ? [ { $limit: count, }, ] : []; const result = await Users.col .aggregate<{ sortedResults: IUser[]; totalCount: { total: number }[] }>([ { $match: nonEmptyQuery, }, { $project: inclusiveFields, }, { $addFields: { nameInsensitive: { $toLower: '$name', }, }, }, { $facet: { sortedResults: [ { $sort: actualSort, }, { $skip: offset, }, ...limit, ], totalCount: [{ $group: { _id: null, total: { $sum: 1 } } }], }, }, ]) .toArray(); const { sortedResults: users, totalCount: [{ total } = { total: 0 }], } = result[0]; return API.v1.success({ users, count: users.length, offset, total, }); }, }, ); API.v1.addRoute( 'users.register', { authRequired: false, rateLimiterOptions: { numRequestsAllowed: settings.get('Rate_Limiter_Limit_RegisterUser') ?? 1, intervalTimeInMS: settings.get('API_Enable_Rate_Limiter_Limit_Time_Default'), }, validateParams: isUserRegisterParamsPOST, }, { async post() { if (this.userId) { return API.v1.failure('Logged in users can not register again.'); } if (!(await checkUsernameAvailability(this.bodyParams.username))) { return API.v1.failure('Username is already in use'); } const { secret: secretURL, ...params } = this.bodyParams; if (this.bodyParams.customFields) { try { await validateCustomFields(this.bodyParams.customFields); } catch (e) { return API.v1.failure(e); } } // Register the user const userId = await Meteor.callAsync('registerUser', { ...params, ...(secretURL && { secretURL }), }); // Now set their username const { fields } = await this.parseJsonQuery(); await setUsernameWithValidation(userId, this.bodyParams.username); const user = await Users.findOneById(userId, { projection: fields }); if (!user) { return API.v1.failure('User not found'); } if (this.bodyParams.customFields) { await saveCustomFields(userId, this.bodyParams.customFields); } return API.v1.success({ user }); }, }, ); API.v1.addRoute( 'users.resetAvatar', { authRequired: true }, { async post() { const user = await getUserFromParams(this.bodyParams); if (settings.get('Accounts_AllowUserAvatarChange') && user._id === this.userId) { await Meteor.callAsync('resetAvatar'); } else if ( (await hasPermissionAsync(this.userId, 'edit-other-user-avatar')) || (await hasPermissionAsync(this.userId, 'manage-moderation-actions')) ) { await Meteor.callAsync('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.createToken', { authRequired: true }, { async post() { const user = await getUserFromParams(this.bodyParams); const data = await Meteor.callAsync('createToken', user._id); return data ? API.v1.success({ data }) : API.v1.unauthorized(); }, }, ); API.v1.addRoute( 'users.getPreferences', { authRequired: true }, { async get() { const user = await Users.findOneById(this.userId); if (user?.settings) { const { preferences = {} } = user?.settings; preferences.language = user?.language; return API.v1.success({ preferences, }); } return API.v1.failure(i18n.t('Accounts_Default_User_Preferences_not_available').toUpperCase()); }, }, ); API.v1.addRoute( 'users.forgotPassword', { authRequired: false }, { async post() { const { email } = this.bodyParams; if (!email) { return API.v1.failure("The 'email' param is required"); } await Meteor.callAsync('sendForgotPasswordEmail', email.toLowerCase()); return API.v1.success(); }, }, ); API.v1.addRoute( 'users.getUsernameSuggestion', { authRequired: true }, { async get() { const result = await Meteor.callAsync('getUsernameSuggestion'); return API.v1.success({ result }); }, }, ); API.v1.addRoute( 'users.checkUsernameAvailability', { authRequired: true, validateParams: isUsersCheckUsernameAvailabilityParamsGET, }, { async get() { const { username } = this.queryParams; const result = await checkUsernameAvailabilityWithValidation(this.userId, username); return API.v1.success({ result }); }, }, ); API.v1.addRoute( 'users.generatePersonalAccessToken', { authRequired: true, twoFactorRequired: true }, { async post() { const { tokenName, bypassTwoFactor } = this.bodyParams; if (!tokenName) { return API.v1.failure("The 'tokenName' param is required"); } const token = await Meteor.callAsync('personalAccessTokens:generateToken', { tokenName, bypassTwoFactor }); return API.v1.success({ token }); }, }, ); API.v1.addRoute( 'users.regeneratePersonalAccessToken', { authRequired: true, twoFactorRequired: true }, { async post() { const { tokenName } = this.bodyParams; if (!tokenName) { return API.v1.failure("The 'tokenName' param is required"); } const token = await Meteor.callAsync('personalAccessTokens:regenerateToken', { tokenName }); return API.v1.success({ token }); }, }, ); API.v1.addRoute( 'users.getPersonalAccessTokens', { authRequired: true }, { async get() { if (!(await hasPermissionAsync(this.userId, 'create-personal-access-tokens'))) { throw new Meteor.Error('not-authorized', 'Not Authorized'); } const user = (await Users.getLoginTokensByUserId(this.userId).toArray())[0] as unknown as IUser | undefined; const isPersonalAccessToken = (loginToken: ILoginToken | IPersonalAccessToken): loginToken is IPersonalAccessToken => 'type' in loginToken && loginToken.type === 'personalAccessToken'; return API.v1.success({ tokens: user?.services?.resume?.loginTokens?.filter(isPersonalAccessToken).map((loginToken) => ({ name: loginToken.name, createdAt: loginToken.createdAt.toISOString(), lastTokenPart: loginToken.lastTokenPart, bypassTwoFactor: Boolean(loginToken.bypassTwoFactor), })) || [], }); }, }, ); API.v1.addRoute( 'users.removePersonalAccessToken', { authRequired: true, twoFactorRequired: true }, { async post() { const { tokenName } = this.bodyParams; if (!tokenName) { return API.v1.failure("The 'tokenName' param is required"); } await Meteor.callAsync('personalAccessTokens:removeToken', { tokenName, }); return API.v1.success(); }, }, ); API.v1.addRoute( 'users.2fa.enableEmail', { authRequired: true }, { async post() { await Users.enableEmail2FAByUserId(this.userId); return API.v1.success(); }, }, ); API.v1.addRoute( 'users.2fa.disableEmail', { authRequired: true, twoFactorRequired: true, twoFactorOptions: { disableRememberMe: true } }, { async post() { await Users.disableEmail2FAByUserId(this.userId); return API.v1.success(); }, }, ); API.v1.addRoute('users.2fa.sendEmailCode', { async post() { const { emailOrUsername } = this.bodyParams; if (!emailOrUsername) { throw new Meteor.Error('error-parameter-required', 'emailOrUsername is required'); } const method = emailOrUsername.includes('@') ? 'findOneByEmailAddress' : 'findOneByUsername'; const userId = this.userId || (await Users[method](emailOrUsername, { projection: { _id: 1 } }))?._id; if (!userId) { // this.logger.error('[2fa] User was not found when requesting 2fa email code'); return API.v1.success(); } const user = await getUserForCheck(userId); if (!user) { // this.logger.error('[2fa] User was not found when requesting 2fa email code'); return API.v1.success(); } await emailCheck.sendEmailCode(user); return API.v1.success(); }, }); API.v1.addRoute( 'users.sendConfirmationEmail', { authRequired: true, validateParams: isUsersSendConfirmationEmailParamsPOST, }, { async post() { const { email } = this.bodyParams; if (await Meteor.callAsync('sendConfirmationEmail', email)) { return API.v1.success(); } return API.v1.failure(); }, }, ); API.v1.addRoute( 'users.presence', { authRequired: true }, { async get() { // if presence broadcast is disabled, return an empty array (all users are "offline") if (settings.get('Presence_broadcast_disabled')) { return API.v1.success({ users: [], full: true, }); } const { from, ids } = this.queryParams; const options = { projection: { username: 1, name: 1, status: 1, utcOffset: 1, statusText: 1, avatarETag: 1, }, }; if (ids) { return API.v1.success({ users: await Users.findNotOfflineByIds(Array.isArray(ids) ? ids : ids.split(','), options).toArray(), full: false, }); } if (from) { const ts = new Date(from); const diff = (Date.now() - Number(ts)) / 1000 / 60; if (diff < 10) { return API.v1.success({ users: await Users.findNotIdUpdatedFrom(this.userId, ts, options).toArray(), full: false, }); } } return API.v1.success({ users: await Users.findUsersNotOffline(options).toArray(), full: true, }); }, }, ); API.v1.addRoute( 'users.requestDataDownload', { authRequired: true }, { async get() { const { fullExport = false } = this.queryParams; const result = (await Meteor.callAsync('requestDataDownload', { fullExport: fullExport === 'true' })) as { requested: boolean; exportOperation: IExportOperation; }; return API.v1.success({ requested: Boolean(result.requested), exportOperation: result.exportOperation, }); }, }, ); API.v1.addRoute( 'users.logoutOtherClients', { authRequired: true }, { async post() { const xAuthToken = this.request.headers['x-auth-token'] as string; if (!xAuthToken) { throw new Meteor.Error('error-parameter-required', 'x-auth-token is required'); } const hashedToken = Accounts._hashLoginToken(xAuthToken); if (!(await Users.removeNonPATLoginTokensExcept(this.userId, hashedToken))) { throw new Meteor.Error('error-invalid-user-id', 'Invalid user id'); } const me = (await Users.findOneById(this.userId, { projection: { 'services.resume.loginTokens': 1 } })) as Pick; const token = me.services?.resume?.loginTokens?.find((token) => token.hashedToken === hashedToken); const tokenExpires = (token && 'when' in token && new Date(token.when.getTime() + settings.get('Accounts_LoginExpiration') * 1000)) || undefined; return API.v1.success({ token: xAuthToken, tokenExpires: tokenExpires?.toISOString() || '', }); }, }, ); API.v1.addRoute( 'users.autocomplete', { authRequired: true, validateParams: isUsersAutocompleteProps }, { async get() { const { selector: selectorRaw } = this.queryParams; const selector: { exceptions: Required['username'][]; conditions: Filter; term: string } = JSON.parse(selectorRaw); try { if (selector?.conditions && !isValidQuery(selector.conditions, ['*'], ['$or', '$and'])) { throw new Error('error-invalid-query'); } } catch (e) { return API.v1.failure(e); } return API.v1.success( await findUsersToAutocomplete({ uid: this.userId, selector, }), ); }, }, ); API.v1.addRoute( 'users.removeOtherTokens', { authRequired: true }, { async post() { return API.v1.success(await Meteor.callAsync('removeOtherTokens')); }, }, ); API.v1.addRoute( 'users.resetE2EKey', { authRequired: true, twoFactorRequired: true, twoFactorOptions: { disableRememberMe: true } }, { async post() { if ('userId' in this.bodyParams || 'username' in this.bodyParams || 'user' in this.bodyParams) { // reset other user keys const user = await getUserFromParams(this.bodyParams); 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 (!(await hasPermissionAsync(this.userId, 'edit-other-user-e2ee'))) { throw new Meteor.Error('error-not-allowed', 'Not allowed'); } if (!(await resetUserE2EEncriptionKey(user._id, true))) { return API.v1.failure(); } return API.v1.success(); } await resetUserE2EEncriptionKey(this.userId, false); return API.v1.success(); }, }, ); API.v1.addRoute( 'users.resetTOTP', { authRequired: true, twoFactorRequired: true, twoFactorOptions: { disableRememberMe: true } }, { async post() { // // reset own keys if ('userId' in this.bodyParams || 'username' in this.bodyParams || 'user' in this.bodyParams) { // reset other user keys if (!(await hasPermissionAsync(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'); } const user = await getUserFromParams(this.bodyParams); if (!user) { throw new Meteor.Error('error-invalid-user-id', 'Invalid user id'); } await resetTOTP(user._id, true); return API.v1.success(); } await resetTOTP(this.userId, false); return API.v1.success(); }, }, ); API.v1.addRoute( 'users.listTeams', { authRequired: true, validateParams: isUsersListTeamsProps }, { async get() { check( this.queryParams, Match.ObjectIncluding({ userId: Match.Maybe(String), }), ); const { userId } = this.queryParams; // If the caller has permission to view all teams, there's no need to filter the teams const adminId = (await hasPermissionAsync(this.userId, 'view-all-teams')) ? undefined : this.userId; const teams = await Team.findBySubscribedUserIds(userId, adminId); return API.v1.success({ teams, }); }, }, ); API.v1.addRoute( 'users.logout', { authRequired: true, validateParams: isUserLogoutParamsPOST }, { async post() { const userId = this.bodyParams.userId || this.userId; if (userId !== this.userId && !(await hasPermissionAsync(this.userId, 'logout-other-user'))) { return API.v1.unauthorized(); } // this method logs the user out automatically, if successful returns 1, otherwise 0 if (!(await Users.unsetLoginTokens(userId))) { throw new Meteor.Error('error-invalid-user-id', 'Invalid user id'); } return API.v1.success({ message: `User ${userId} has been logged out!`, }); }, }, ); API.v1.addRoute( 'users.getPresence', { authRequired: true }, { async get() { if (isUserFromParams(this.queryParams, this.userId, this.user)) { const user = await Users.findOneById(this.userId); return API.v1.success({ presence: (user?.status || 'offline') as UserStatus, connectionStatus: user?.statusConnection || 'offline', ...(user?.lastLogin && { lastLogin: user?.lastLogin }), }); } const user = await getUserFromParams(this.queryParams); return API.v1.success({ presence: user.status || ('offline' as UserStatus), }); }, }, ); API.v1.addRoute( 'users.setStatus', { authRequired: true }, { async post() { check( this.bodyParams, Match.OneOf( Match.ObjectIncluding({ status: Match.Maybe(String), message: String, }), Match.ObjectIncluding({ status: 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 = await (async (): Promise< Pick | undefined | null > => { if (isUserFromParams(this.bodyParams, this.userId, this.user)) { return Users.findOneById(this.userId); } if (await hasPermissionAsync(this.userId, 'edit-other-user-info')) { return getUserFromParams(this.bodyParams); } })(); if (!user) { return API.v1.unauthorized(); } if (this.bodyParams.message || this.bodyParams.message === '') { await 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', }); } await Users.updateOne( { _id: user._id }, { $set: { status, statusDefault: status, }, }, ); const { _id, username, statusText, roles, name } = user; void api.broadcast('presence.status', { user: { status, _id, username, statusText, roles, name }, previousStatus: 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 }, { async get() { if (isUserFromParams(this.queryParams, this.userId, this.user)) { const user: IUser | null = await 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 = await getUserFromParams(this.queryParams); return API.v1.success({ _id: user._id, // message: user.statusText, status: (user.status || 'offline') as 'online' | 'offline' | 'away' | 'busy', }); }, }, ); settings.watch('Rate_Limiter_Limit_RegisterUser', (value) => { const userRegisterRoute = '/api/v1/users.registerpost'; API.v1.updateRateLimiterDictionaryForRoute(userRegisterRoute, value); });