The communications platform that puts data protection first.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
Rocket.Chat/apps/meteor/app/api/server/v1/users.ts

1239 lines
32 KiB

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<Pick<IUser, '_id' | 'settings'>>,
});
},
},
);
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<IUser, '_id' | 'roles' | 'username' | 'name' | 'status' | 'statusText'> | 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<IUser, 'services'>;
const token = me.services?.resume?.loginTokens?.find((token) => token.hashedToken === hashedToken);
const tokenExpires =
(token && 'when' in token && new Date(token.when.getTime() + settings.get<number>('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<IUser>['username'][]; conditions: Filter<IUser>; 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<IUser, '_id' | 'username' | 'name' | 'status' | 'statusText' | 'roles'> | 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<number>('Rate_Limiter_Limit_RegisterUser', (value) => {
const userRegisterRoute = '/api/v1/users.registerpost';
API.v1.updateRateLimiterDictionaryForRoute(userRegisterRoute, value);
});