From 35a3b76cd88049723bcb71b3964472221f0e410f Mon Sep 17 00:00:00 2001 From: rique223 Date: Tue, 5 Mar 2024 14:07:39 -0300 Subject: [PATCH 01/15] refactor: :recycle: Create isSMTPConfigured helper Created a helper function named isSMTPConfigured.ts to run when calling the smtp.check endpoint of the misc.ts group. --- apps/meteor/app/api/server/v1/misc.ts | 5 ++--- apps/meteor/app/utils/server/functions/isSMTPConfigured.ts | 6 ++++++ 2 files changed, 8 insertions(+), 3 deletions(-) create mode 100644 apps/meteor/app/utils/server/functions/isSMTPConfigured.ts diff --git a/apps/meteor/app/api/server/v1/misc.ts b/apps/meteor/app/api/server/v1/misc.ts index 7b6c964a50bb..b8c5c06b2eba 100644 --- a/apps/meteor/app/api/server/v1/misc.ts +++ b/apps/meteor/app/api/server/v1/misc.ts @@ -27,6 +27,7 @@ import { passwordPolicy } from '../../../lib/server'; import { apiDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; import { settings } from '../../../settings/server'; import { getDefaultUserFields } from '../../../utils/server/functions/getDefaultUserFields'; +import { isSMTPConfigured } from '../../../utils/server/functions/isSMTPConfigured'; import { getURL } from '../../../utils/server/getURL'; import { API } from '../api'; import { getLoggedInUser } from '../helpers/getLoggedInUser'; @@ -634,9 +635,7 @@ API.v1.addRoute( { authRequired: true }, { async get() { - const isMailURLSet = !(process.env.MAIL_URL === 'undefined' || process.env.MAIL_URL === undefined); - const isSMTPConfigured = Boolean(settings.get('SMTP_Host')) || isMailURLSet; - return API.v1.success({ isSMTPConfigured }); + return API.v1.success({ isSMTPConfigured: isSMTPConfigured() }); }, }, ); diff --git a/apps/meteor/app/utils/server/functions/isSMTPConfigured.ts b/apps/meteor/app/utils/server/functions/isSMTPConfigured.ts new file mode 100644 index 000000000000..fa300cb37ee2 --- /dev/null +++ b/apps/meteor/app/utils/server/functions/isSMTPConfigured.ts @@ -0,0 +1,6 @@ +import { settings } from '../../../settings/server'; + +export const isSMTPConfigured = (): boolean => { + const isMailURLSet = !(process.env.MAIL_URL === 'undefined' || process.env.MAIL_URL === undefined); + return Boolean(settings.get('SMTP_Host')) || isMailURLSet; +}; From c7ea10e8c530ad503c9bb6d04272348837df5a09 Mon Sep 17 00:00:00 2001 From: rique223 Date: Tue, 5 Mar 2024 15:08:46 -0300 Subject: [PATCH 02/15] feat: :sparkles: Create users.listByStatys and users.sendWelcomeEmail endpoints Created the two new endpoints that represent the back-end for the new users page. --- apps/meteor/app/api/server/v1/users.ts | 127 ++++++++++++++++++ .../meteor/server/methods/sendWelcomeEmail.ts | 49 +++++++ packages/rest-typings/src/v1/users.ts | 21 ++- .../src/v1/users/UsersListStatusParamsGET.ts | 50 +++++++ .../users/UsersSendWelcomeEmailParamsPOST.ts | 20 +++ 5 files changed, 266 insertions(+), 1 deletion(-) create mode 100644 apps/meteor/server/methods/sendWelcomeEmail.ts create mode 100644 packages/rest-typings/src/v1/users/UsersListStatusParamsGET.ts create mode 100644 packages/rest-typings/src/v1/users/UsersSendWelcomeEmailParamsPOST.ts diff --git a/apps/meteor/app/api/server/v1/users.ts b/apps/meteor/app/api/server/v1/users.ts index 10ea2f0b5ac2..e49b63d75591 100644 --- a/apps/meteor/app/api/server/v1/users.ts +++ b/apps/meteor/app/api/server/v1/users.ts @@ -6,6 +6,8 @@ import { isUserSetActiveStatusParamsPOST, isUserDeactivateIdleParamsPOST, isUsersInfoParamsGetProps, + isUsersListStatusProps, + isUsersSendWelcomeEmailProps, isUserRegisterParamsPOST, isUserLogoutParamsPOST, isUsersListTeamsProps, @@ -17,6 +19,7 @@ import { isUsersCheckUsernameAvailabilityParamsGET, isUsersSendConfirmationEmailParamsPOST, } from '@rocket.chat/rest-typings'; +import { escapeRegExp } from '@rocket.chat/string-helpers'; import { Accounts } from 'meteor/accounts-base'; import { Match, check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; @@ -25,6 +28,7 @@ import type { Filter } from 'mongodb'; import { i18n } from '../../../../server/lib/i18n'; import { resetUserE2EEncriptionKey } from '../../../../server/lib/resetUserE2EKey'; import { saveUserPreferences } from '../../../../server/methods/saveUserPreferences'; +import { sendWelcomeEmail } from '../../../../server/methods/sendWelcomeEmail'; import { getUserForCheck, emailCheck } from '../../../2fa/server/code'; import { resetTOTP } from '../../../2fa/server/functions/resetTOTP'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; @@ -555,6 +559,129 @@ API.v1.addRoute( }, ); +API.v1.addRoute( + 'users.listByStatus', + { + authRequired: true, + validateParams: isUsersListStatusProps, + permissionsRequired: ['view-d-room', 'view-outside-room'], + }, + { + async get() { + const { offset, count } = await getPaginationItems(this.queryParams); + const { sort, fields } = await this.parseJsonQuery(); + const { status, roles, searchTerm } = this.queryParams; + + const projection = { + name: 1, + username: 1, + emails: 1, + roles: 1, + status: 1, + active: 1, + avatarETag: 1, + lastLogin: 1, + type: 1, + reason: 0, + ...fields, + }; + + const actualSort: Record = sort || { username: 1 }; + + if (sort?.status) { + actualSort.active = sort.status; + } + + if (sort?.name) { + actualSort.nameInsensitive = sort.name; + } + + let match: Filter; + + switch (status) { + case 'active': + match = { + active: true, + lastLogin: { $exists: true }, + }; + break; + case 'all': + match = {}; + break; + case 'deactivated': + match = { + active: false, + lastLogin: { $exists: true }, + }; + break; + case 'pending': + match = { + lastLogin: { $exists: false }, + type: { $nin: ['bot', 'app'] }, + }; + projection.reason = 1; + break; + default: + throw new Meteor.Error('invalid-params', 'Invalid status parameter'); + } + + const canSeeAllUserInfo = await hasPermissionAsync(this.userId, 'view-full-other-user-info'); + + match = { + ...match, + $or: [ + ...(canSeeAllUserInfo ? [{ 'emails.address': { $regex: escapeRegExp(searchTerm), $options: 'i' } }] : []), + { username: { $regex: escapeRegExp(searchTerm), $options: 'i' } }, + { name: { $regex: escapeRegExp(searchTerm), $options: 'i' } }, + ], + }; + + if (roles?.length && !roles.includes('all')) { + match = { + ...match, + roles: { $in: roles }, + }; + } + + const { cursor, totalCount } = await Users.findPaginated( + { + ...match, + }, + { + sort: actualSort, + skip: offset, + limit: count, + projection, + }, + ); + const [users, total] = await Promise.all([cursor.toArray(), totalCount]); + + return API.v1.success({ + users, + count: users.length, + offset, + total, + }); + }, + }, +); + +API.v1.addRoute( + 'users.sendWelcomeEmail', + { + authRequired: true, + validateParams: isUsersSendWelcomeEmailProps, + }, + { + async post() { + const { email } = this.bodyParams; + await sendWelcomeEmail(email); + + return API.v1.success(); + }, + }, +); + API.v1.addRoute( 'users.register', { diff --git a/apps/meteor/server/methods/sendWelcomeEmail.ts b/apps/meteor/server/methods/sendWelcomeEmail.ts new file mode 100644 index 000000000000..a31d642ff73a --- /dev/null +++ b/apps/meteor/server/methods/sendWelcomeEmail.ts @@ -0,0 +1,49 @@ +import { Users } from '@rocket.chat/models'; +import { Meteor } from 'meteor/meteor'; + +import * as Mailer from '../../app/mailer/server/api'; +import { settings } from '../../app/settings/server'; +import { isSMTPConfigured } from '../../app/utils/server/functions/isSMTPConfigured'; + +export async function sendWelcomeEmail(to: string): Promise { + if (typeof to !== 'string') { + throw new Meteor.Error('error-invalid-arguments', 'Invalid arguments', { + method: 'sendWelcomeEmail', + }); + } + + if (!isSMTPConfigured()) { + throw new Meteor.Error('error-email-send-failed', 'SMTP is not configured', { + method: 'sendWelcomeEmail', + }); + } + + const email = to.trim(); + + const user = await Users.findOneByEmailAddress(email, { projection: { _id: 1 } }); + + if (!user) { + throw new Meteor.Error('error-invalid-user', 'Invalid user', { + method: 'sendWelcomeEmail', + }); + } + + try { + let html = ''; + Mailer.getTemplate('Accounts_UserAddedEmail_Email', (template) => { + html = template; + }); + + await Mailer.send({ + to: email, + from: settings.get('From_Email'), + subject: settings.get('Accounts_UserAddedEmail_Subject'), + html, + }); + } catch (error: any) { + throw new Meteor.Error('error-email-send-failed', `Error trying to send email: ${error.message}`, { + method: 'sendWelcomeEmail', + message: error.message, + }); + } +} diff --git a/packages/rest-typings/src/v1/users.ts b/packages/rest-typings/src/v1/users.ts index 15f9ad840d42..6ec08410b034 100644 --- a/packages/rest-typings/src/v1/users.ts +++ b/packages/rest-typings/src/v1/users.ts @@ -10,8 +10,10 @@ import type { UserRegisterParamsPOST } from './users/UserRegisterParamsPOST'; import type { UserSetActiveStatusParamsPOST } from './users/UserSetActiveStatusParamsPOST'; import type { UsersAutocompleteParamsGET } from './users/UsersAutocompleteParamsGET'; import type { UsersInfoParamsGet } from './users/UsersInfoParamsGet'; +import type { UsersListStatusParamsGET } from './users/UsersListStatusParamsGET'; import type { UsersListTeamsParamsGET } from './users/UsersListTeamsParamsGET'; import type { UsersSendConfirmationEmailParamsPOST } from './users/UsersSendConfirmationEmailParamsPOST'; +import type { UsersSendWelcomeEmailParamsPOST } from './users/UsersSendWelcomeEmailParamsPOST'; import type { UsersSetPreferencesParamsPOST } from './users/UsersSetPreferenceParamsPOST'; import type { UsersUpdateOwnBasicInfoParamsPOST } from './users/UsersUpdateOwnBasicInfoParamsPOST'; import type { UsersUpdateParamsPOST } from './users/UsersUpdateParamsPOST'; @@ -110,6 +112,11 @@ export type UserPresence = Readonly< export type UserPersonalTokens = Pick & { createdAt: string }; +export type PickedUser = Pick< + IUser, + '_id' | 'username' | 'name' | 'status' | 'roles' | 'emails' | 'active' | 'avatarETag' | 'lastLogin' | 'type' +>; + export type UsersEndpoints = { '/v1/users.2fa.enableEmail': { POST: () => void; @@ -139,10 +146,20 @@ export type UsersEndpoints = { '/v1/users.list': { GET: (params: PaginatedRequest<{ fields: string }>) => PaginatedResult<{ - users: Pick[]; + users: PickedUser[][]; }>; }; + '/v1/users.listByStatus': { + GET: (params: UsersListStatusParamsGET) => PaginatedResult<{ + users: PickedUser[]; + }>; + }; + + '/v1/users.sendWelcomeEmail': { + POST: (params: UsersSendWelcomeEmailParamsPOST) => { success: boolean }; + }; + '/v1/users.setAvatar': { POST: (params: UsersSetAvatar) => void; }; @@ -373,6 +390,8 @@ export * from './users/UserCreateParamsPOST'; export * from './users/UserSetActiveStatusParamsPOST'; export * from './users/UserDeactivateIdleParamsPOST'; export * from './users/UsersInfoParamsGet'; +export * from './users/UsersListStatusParamsGET'; +export * from './users/UsersSendWelcomeEmailParamsPOST'; export * from './users/UserRegisterParamsPOST'; export * from './users/UserLogoutParamsPOST'; export * from './users/UsersListTeamsParamsGET'; diff --git a/packages/rest-typings/src/v1/users/UsersListStatusParamsGET.ts b/packages/rest-typings/src/v1/users/UsersListStatusParamsGET.ts new file mode 100644 index 000000000000..a991790911be --- /dev/null +++ b/packages/rest-typings/src/v1/users/UsersListStatusParamsGET.ts @@ -0,0 +1,50 @@ +import Ajv from 'ajv'; + +import type { PaginatedRequest } from '../../helpers/PaginatedRequest'; + +const ajv = new Ajv({ + coerceTypes: true, +}); + +export type UsersListStatusParamsGET = PaginatedRequest<{ + status: 'active' | 'all' | 'deactivated' | 'pending'; + roles?: string[]; + searchTerm?: string; +}>; + +const UsersListStatusParamsGetSchema = { + type: 'object', + properties: { + status: { + type: 'string', + enum: ['active', 'all', 'deactivated', 'pending'], + }, + roles: { + type: 'array', + items: { + type: 'string', + }, + nullable: true, + }, + searchTerm: { + type: 'string', + nullable: true, + }, + sort: { + type: 'string', + nullable: true, + }, + count: { + type: 'number', + nullable: true, + }, + offset: { + type: 'number', + nullable: true, + }, + }, + required: ['status'], + additionalProperties: false, +}; + +export const isUsersListStatusProps = ajv.compile(UsersListStatusParamsGetSchema); diff --git a/packages/rest-typings/src/v1/users/UsersSendWelcomeEmailParamsPOST.ts b/packages/rest-typings/src/v1/users/UsersSendWelcomeEmailParamsPOST.ts new file mode 100644 index 000000000000..7bfd6f04c021 --- /dev/null +++ b/packages/rest-typings/src/v1/users/UsersSendWelcomeEmailParamsPOST.ts @@ -0,0 +1,20 @@ +import Ajv from 'ajv'; + +const ajv = new Ajv({ + coerceTypes: true, +}); + +export type UsersSendWelcomeEmailParamsPOST = { email: string }; + +const UsersSendWelcomeEmailParamsPostSchema = { + type: 'object', + properties: { + email: { + type: 'string', + }, + }, + required: ['email'], + additionalProperties: false, +}; + +export const isUsersSendWelcomeEmailProps = ajv.compile(UsersSendWelcomeEmailParamsPostSchema); From ed5cfec74d0372fc1d648cf9da0539fd6ffeed60 Mon Sep 17 00:00:00 2001 From: rique223 Date: Tue, 5 Mar 2024 15:33:08 -0300 Subject: [PATCH 03/15] test: :white_check_mark: Create tests for the users.listByStatus and users.sendWelcomeEmail endpoints --- apps/meteor/tests/end-to-end/api/01-users.js | 257 +++++++++++++++++++ 1 file changed, 257 insertions(+) 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 1aa30d3322bb..04e1120db4c5 100644 --- a/apps/meteor/tests/end-to-end/api/01-users.js +++ b/apps/meteor/tests/end-to-end/api/01-users.js @@ -4285,4 +4285,261 @@ describe('[Users]', function () { }); }); }); + + describe('[/users.listByStatus]', () => { + let user; + let otherUser; + let otherUserCredentials; + + before(async () => { + user = await createUser(); + otherUser = await createUser(); + otherUserCredentials = await login(otherUser.username, password); + }); + + after(async () => { + await deleteUser(user); + await deleteUser(otherUser); + await updatePermission('view-outside-room', ['admin', 'owner', 'moderator', 'user']); + await updatePermission('view-d-room', ['admin', 'owner', 'moderator', 'user']); + }); + + it('should list pending users', async () => { + await request + .get(api('users.listByStatus')) + .set(credentials) + .query({ status: 'pending', count: 50 }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('users'); + + const { users } = res.body; + const ids = users.map((user) => user._id); + + expect(ids).to.include(user._id); + }); + }); + + it('should list all users', async () => { + await login(user.username, password); + + await request + .get(api('users.listByStatus')) + .set(credentials) + .query({ status: 'all' }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('users'); + + const { users } = res.body; + const ids = users.map((user) => user._id); + + expect(ids).to.include(user._id); + }); + }); + + it('should list active users', async () => { + await login(user.username, password); + + await request + .get(api('users.listByStatus')) + .set(credentials) + .query({ status: 'active' }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('users'); + + const { users } = res.body; + const ids = users.map((user) => user._id); + + expect(ids).to.include(user._id); + }); + }); + + it('should filter users by role', async () => { + await login(user.username, password); + + await request + .get(api('users.listByStatus')) + .set(credentials) + .query({ 'status': 'active', 'roles[]': 'admin' }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('users'); + + const { users } = res.body; + const ids = users.map((user) => user._id); + + expect(ids).to.not.include(user._id); + }); + }); + + it('should list deactivated users', async () => { + await request.post(api('users.setActiveStatus')).set(credentials).send({ + userId: user._id, + activeStatus: false, + confirmRelinquish: false, + }); + + await request + .get(api('users.listByStatus')) + .set(credentials) + .query({ status: 'deactivated' }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('users'); + + const { users } = res.body; + const ids = users.map((user) => user._id); + + expect(ids).to.include(user._id); + }); + }); + + it('should filter users by username', async () => { + await request + .get(api('users.listByStatus')) + .set(credentials) + .query({ status: 'all', searchTerm: user.username }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('users'); + + const { users } = res.body; + const ids = users.map((user) => user._id); + + expect(ids).to.include(user._id); + }); + }); + + it('should return error for invalid status params', async () => { + await request + .get(api('users.listByStatus')) + .set(credentials) + .query({ status: 'abcd' }) + .expect('Content-Type', 'application/json') + .expect(400) + .expect((res) => { + expect(res.body).to.have.property('success', false); + expect(res.body.errorType).to.be.equal('invalid-params'); + expect(res.body.error).to.be.equal('must be equal to one of the allowed values [invalid-params]'); + }); + }); + + it('should throw unauthorized error to user without "view-d-room" permission', async () => { + await updatePermission('view-d-room', ['admin']); + await request + .get(api('users.listByStatus')) + .set(otherUserCredentials) + .query({ status: 'active' }) + .expect('Content-Type', 'application/json') + .expect(403) + .expect((res) => { + expect(res.body).to.have.property('success', false); + expect(res.body.error).to.be.equal('User does not have the permissions required for this action [error-unauthorized]'); + }); + }); + + it('should throw unauthorized error to user without "view-outside-room" permission', async () => { + await updatePermission('view-outside-room', ['admin']); + await request + .get(api('users.listByStatus')) + .set(otherUserCredentials) + .query({ status: 'active' }) + .expect('Content-Type', 'application/json') + .expect(403) + .expect((res) => { + expect(res.body).to.have.property('success', false); + expect(res.body.error).to.be.equal('User does not have the permissions required for this action [error-unauthorized]'); + }); + }); + }); + + describe('[/users.sendWelcomeEmail]', async () => { + let user; + let otherUser; + + before(async () => { + user = await createUser(); + otherUser = await createUser(); + }); + + after(async () => { + await deleteUser(user); + await deleteUser(otherUser); + }); + + it('should send Welcome Email to user', async () => { + await updateSetting('SMTP_Host', 'localhost'); + + await request + .post(api('users.sendWelcomeEmail')) + .set(credentials) + .send({ email: user.emails[0].address }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + }); + }); + + it('should fail to send Welcome Email due to SMTP settings missing', async () => { + await updateSetting('SMTP_Host', ''); + + await request + .post(api('users.sendWelcomeEmail')) + .set(credentials) + .send({ email: user.emails[0].address }) + .expect('Content-Type', 'application/json') + .expect(400) + .expect((res) => { + expect(res.body).to.have.property('success', false); + expect(res.body.error).to.be.equal('SMTP is not configured [error-email-send-failed]'); + }); + }); + + it('should fail to send Welcome Email due to missing param', async () => { + await updateSetting('SMTP_Host', ''); + + await request + .post(api('users.sendWelcomeEmail')) + .set(credentials) + .send({}) + .expect('Content-Type', 'application/json') + .expect(400) + .expect((res) => { + expect(res.body).to.have.property('success', false); + expect(res.body).to.have.property('errorType', 'invalid-params'); + expect(res.body).to.have.property('error', "must have required property 'email' [invalid-params]"); + }); + }); + + it('should fail to send Welcome Email due missing user', async () => { + await updateSetting('SMTP_Host', 'localhost'); + + await request + .post(api('users.sendWelcomeEmail')) + .set(credentials) + .send({ email: 'fake_user32132131231@rocket.chat' }) + .expect('Content-Type', 'application/json') + .expect(400) + .expect((res) => { + expect(res.body).to.have.property('success', false); + expect(res.body).to.have.property('errorType', 'error-invalid-user'); + expect(res.body).to.have.property('error', 'Invalid user [error-invalid-user]'); + }); + }); + }); }); From 4b1549793e0b10b34b039d92b0445d8587432e47 Mon Sep 17 00:00:00 2001 From: rique223 Date: Tue, 5 Mar 2024 16:09:51 -0300 Subject: [PATCH 04/15] Typecheck --- .../client/views/admin/users/UsersTable/UsersTableRow.tsx | 5 +++-- packages/rest-typings/src/v1/users.ts | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/apps/meteor/client/views/admin/users/UsersTable/UsersTableRow.tsx b/apps/meteor/client/views/admin/users/UsersTable/UsersTableRow.tsx index 4010b118e57d..b7460123af7d 100644 --- a/apps/meteor/client/views/admin/users/UsersTable/UsersTableRow.tsx +++ b/apps/meteor/client/views/admin/users/UsersTable/UsersTableRow.tsx @@ -1,5 +1,6 @@ -import type { IRole, IUser } from '@rocket.chat/core-typings'; +import type { IRole, IUser, Serialized } from '@rocket.chat/core-typings'; import { Box } from '@rocket.chat/fuselage'; +import type { PickedUser } from '@rocket.chat/rest-typings'; import { capitalize } from '@rocket.chat/string-helpers'; import { UserAvatar } from '@rocket.chat/ui-avatar'; import type { TranslationKey } from '@rocket.chat/ui-contexts'; @@ -11,7 +12,7 @@ import { Roles } from '../../../../../app/models/client'; import { GenericTableRow, GenericTableCell } from '../../../../components/GenericTable'; type UsersTableRowProps = { - user: Pick; + user: Serialized; onClick: (id: IUser['_id']) => void; mediaQuery: boolean; }; diff --git a/packages/rest-typings/src/v1/users.ts b/packages/rest-typings/src/v1/users.ts index 6ec08410b034..eab2b421a14f 100644 --- a/packages/rest-typings/src/v1/users.ts +++ b/packages/rest-typings/src/v1/users.ts @@ -146,7 +146,7 @@ export type UsersEndpoints = { '/v1/users.list': { GET: (params: PaginatedRequest<{ fields: string }>) => PaginatedResult<{ - users: PickedUser[][]; + users: PickedUser[]; }>; }; From f5d306835740dad66149722278a799458b74c5d2 Mon Sep 17 00:00:00 2001 From: rique223 Date: Fri, 8 Mar 2024 16:41:41 -0300 Subject: [PATCH 05/15] Reviews first part --- apps/meteor/app/api/server/lib/users.ts | 92 +++++++++++++++ apps/meteor/app/api/server/v1/users.ts | 106 +++--------------- .../{methods => lib}/sendWelcomeEmail.ts | 6 - .../users/UsersSendWelcomeEmailParamsPOST.ts | 7 +- 4 files changed, 107 insertions(+), 104 deletions(-) rename apps/meteor/server/{methods => lib}/sendWelcomeEmail.ts (89%) diff --git a/apps/meteor/app/api/server/lib/users.ts b/apps/meteor/app/api/server/lib/users.ts index 990fda0a0209..ff482e1f9b75 100644 --- a/apps/meteor/app/api/server/lib/users.ts +++ b/apps/meteor/app/api/server/lib/users.ts @@ -119,3 +119,95 @@ export function getNonEmptyQuery(query: Mongo.Query | undefi return { ...defaultQuery, ...query }; } + +type findPaginatedUsersByStatusProps = { + uid: string; + offset: number; + count: number; + sort: Record; + status: 'active' | 'all' | 'deactivated' | 'pending'; + roles: string[] | null; + searchTerm: string; +}; + +export async function findPaginatedUsersByStatus({ uid, offset, count, sort, status, roles, searchTerm }: findPaginatedUsersByStatusProps) { + const projection = { + name: 1, + username: 1, + emails: 1, + roles: 1, + status: 1, + active: 1, + avatarETag: 1, + lastLogin: 1, + type: 1, + reason: 0, + }; + + const actualSort: Record = sort || { username: 1 }; + + if (sort?.status) { + actualSort.active = sort.status; + } + + if (sort?.name) { + actualSort.nameInsensitive = sort.name; + } + + const match: Filter = { $or: [] }; + + switch (status) { + case 'active': + match.active = true; + match.lastLogin = { $exists: true }; + break; + case 'all': + break; + case 'deactivated': + match.active = false; + match.lastLogin = { $exists: true }; + break; + case 'pending': + match.lastLogin = { $exists: false }; + match.type = { $nin: ['bot', 'app'] }; + projection.reason = 1; + break; + } + + const canSeeAllUserInfo = await hasPermissionAsync(uid, 'view-full-other-user-info'); + + match.$or = [ + ...(canSeeAllUserInfo ? [{ 'emails.address': { $regex: escapeRegExp(searchTerm || ''), $options: 'i' } }] : []), + { + username: { $regex: escapeRegExp(searchTerm || ''), $options: 'i' }, + }, + { + name: { $regex: escapeRegExp(searchTerm || ''), $options: 'i' }, + }, + ]; + + if (roles?.length && !roles.includes('all')) { + match.roles = { $in: roles }; + } + + const { cursor, totalCount } = await Users.findPaginated( + { + ...match, + }, + { + sort: actualSort, + skip: offset, + limit: count, + projection, + }, + ); + + const [users, total] = await Promise.all([cursor.toArray(), totalCount]); + + return { + users, + count: users.length, + offset, + total, + }; +} diff --git a/apps/meteor/app/api/server/v1/users.ts b/apps/meteor/app/api/server/v1/users.ts index e49b63d75591..7d4530724abf 100644 --- a/apps/meteor/app/api/server/v1/users.ts +++ b/apps/meteor/app/api/server/v1/users.ts @@ -19,7 +19,6 @@ import { isUsersCheckUsernameAvailabilityParamsGET, isUsersSendConfirmationEmailParamsPOST, } from '@rocket.chat/rest-typings'; -import { escapeRegExp } from '@rocket.chat/string-helpers'; import { Accounts } from 'meteor/accounts-base'; import { Match, check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; @@ -27,8 +26,8 @@ import type { Filter } from 'mongodb'; import { i18n } from '../../../../server/lib/i18n'; import { resetUserE2EEncriptionKey } from '../../../../server/lib/resetUserE2EKey'; +import { sendWelcomeEmail } from '../../../../server/lib/sendWelcomeEmail'; import { saveUserPreferences } from '../../../../server/methods/saveUserPreferences'; -import { sendWelcomeEmail } from '../../../../server/methods/sendWelcomeEmail'; import { getUserForCheck, emailCheck } from '../../../2fa/server/code'; import { resetTOTP } from '../../../2fa/server/functions/resetTOTP'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; @@ -52,7 +51,7 @@ import { getUserFromParams } from '../helpers/getUserFromParams'; import { isUserFromParams } from '../helpers/isUserFromParams'; import { getUploadFormData } from '../lib/getUploadFormData'; import { isValidQuery } from '../lib/isValidQuery'; -import { findUsersToAutocomplete, getInclusiveFields, getNonEmptyFields, getNonEmptyQuery } from '../lib/users'; +import { findPaginatedUsersByStatus, findUsersToAutocomplete, getInclusiveFields, getNonEmptyFields, getNonEmptyQuery } from '../lib/users'; API.v1.addRoute( 'users.getAvatar', @@ -569,99 +568,20 @@ API.v1.addRoute( { async get() { const { offset, count } = await getPaginationItems(this.queryParams); - const { sort, fields } = await this.parseJsonQuery(); + const { sort } = await this.parseJsonQuery(); const { status, roles, searchTerm } = this.queryParams; - const projection = { - name: 1, - username: 1, - emails: 1, - roles: 1, - status: 1, - active: 1, - avatarETag: 1, - lastLogin: 1, - type: 1, - reason: 0, - ...fields, - }; - - const actualSort: Record = sort || { username: 1 }; - - if (sort?.status) { - actualSort.active = sort.status; - } - - if (sort?.name) { - actualSort.nameInsensitive = sort.name; - } - - let match: Filter; - - switch (status) { - case 'active': - match = { - active: true, - lastLogin: { $exists: true }, - }; - break; - case 'all': - match = {}; - break; - case 'deactivated': - match = { - active: false, - lastLogin: { $exists: true }, - }; - break; - case 'pending': - match = { - lastLogin: { $exists: false }, - type: { $nin: ['bot', 'app'] }, - }; - projection.reason = 1; - break; - default: - throw new Meteor.Error('invalid-params', 'Invalid status parameter'); - } - - const canSeeAllUserInfo = await hasPermissionAsync(this.userId, 'view-full-other-user-info'); - - match = { - ...match, - $or: [ - ...(canSeeAllUserInfo ? [{ 'emails.address': { $regex: escapeRegExp(searchTerm), $options: 'i' } }] : []), - { username: { $regex: escapeRegExp(searchTerm), $options: 'i' } }, - { name: { $regex: escapeRegExp(searchTerm), $options: 'i' } }, - ], - }; - - if (roles?.length && !roles.includes('all')) { - match = { - ...match, - roles: { $in: roles }, - }; - } - - const { cursor, totalCount } = await Users.findPaginated( - { - ...match, - }, - { - sort: actualSort, - skip: offset, - limit: count, - projection, - }, + return API.v1.success( + await findPaginatedUsersByStatus({ + uid: this.userId, + offset, + count, + sort, + status, + roles, + searchTerm, + }), ); - const [users, total] = await Promise.all([cursor.toArray(), totalCount]); - - return API.v1.success({ - users, - count: users.length, - offset, - total, - }); }, }, ); diff --git a/apps/meteor/server/methods/sendWelcomeEmail.ts b/apps/meteor/server/lib/sendWelcomeEmail.ts similarity index 89% rename from apps/meteor/server/methods/sendWelcomeEmail.ts rename to apps/meteor/server/lib/sendWelcomeEmail.ts index a31d642ff73a..3daa8df94555 100644 --- a/apps/meteor/server/methods/sendWelcomeEmail.ts +++ b/apps/meteor/server/lib/sendWelcomeEmail.ts @@ -6,12 +6,6 @@ import { settings } from '../../app/settings/server'; import { isSMTPConfigured } from '../../app/utils/server/functions/isSMTPConfigured'; export async function sendWelcomeEmail(to: string): Promise { - if (typeof to !== 'string') { - throw new Meteor.Error('error-invalid-arguments', 'Invalid arguments', { - method: 'sendWelcomeEmail', - }); - } - if (!isSMTPConfigured()) { throw new Meteor.Error('error-email-send-failed', 'SMTP is not configured', { method: 'sendWelcomeEmail', diff --git a/packages/rest-typings/src/v1/users/UsersSendWelcomeEmailParamsPOST.ts b/packages/rest-typings/src/v1/users/UsersSendWelcomeEmailParamsPOST.ts index 7bfd6f04c021..59e85db8a5cb 100644 --- a/packages/rest-typings/src/v1/users/UsersSendWelcomeEmailParamsPOST.ts +++ b/packages/rest-typings/src/v1/users/UsersSendWelcomeEmailParamsPOST.ts @@ -1,8 +1,4 @@ -import Ajv from 'ajv'; - -const ajv = new Ajv({ - coerceTypes: true, -}); +import { ajv } from '../Ajv'; export type UsersSendWelcomeEmailParamsPOST = { email: string }; @@ -11,6 +7,7 @@ const UsersSendWelcomeEmailParamsPostSchema = { properties: { email: { type: 'string', + format: 'email', }, }, required: ['email'], From f3e17312f197daa5018de910efb78f6b14dc82cd Mon Sep 17 00:00:00 2001 From: rique223 Date: Fri, 8 Mar 2024 16:47:42 -0300 Subject: [PATCH 06/15] Reviews second part --- apps/meteor/app/api/server/v1/users.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/apps/meteor/app/api/server/v1/users.ts b/apps/meteor/app/api/server/v1/users.ts index 7d4530724abf..f9c8feed16ba 100644 --- a/apps/meteor/app/api/server/v1/users.ts +++ b/apps/meteor/app/api/server/v1/users.ts @@ -563,10 +563,17 @@ API.v1.addRoute( { authRequired: true, validateParams: isUsersListStatusProps, - permissionsRequired: ['view-d-room', 'view-outside-room'], + permissionsRequired: ['view-d-room'], }, { async get() { + 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 } = await this.parseJsonQuery(); const { status, roles, searchTerm } = this.queryParams; From 1208a885dfb88c8d5f036bc6ba2cfbb0295be1b1 Mon Sep 17 00:00:00 2001 From: rique223 Date: Fri, 8 Mar 2024 18:40:43 -0300 Subject: [PATCH 07/15] Reviews third part --- apps/meteor/app/api/server/lib/users.ts | 4 ++-- apps/meteor/app/api/server/v1/users.ts | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/meteor/app/api/server/lib/users.ts b/apps/meteor/app/api/server/lib/users.ts index ff482e1f9b75..22f18495b6d0 100644 --- a/apps/meteor/app/api/server/lib/users.ts +++ b/apps/meteor/app/api/server/lib/users.ts @@ -2,7 +2,7 @@ import type { IUser } from '@rocket.chat/core-typings'; import { Users, Subscriptions } from '@rocket.chat/models'; import { escapeRegExp } from '@rocket.chat/string-helpers'; import type { Mongo } from 'meteor/mongo'; -import type { Filter } from 'mongodb'; +import type { Filter, RootFilterOperators } from 'mongodb'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; import { settings } from '../../../settings/server'; @@ -154,7 +154,7 @@ export async function findPaginatedUsersByStatus({ uid, offset, count, sort, sta actualSort.nameInsensitive = sort.name; } - const match: Filter = { $or: [] }; + const match: Filter> = {}; switch (status) { case 'active': diff --git a/apps/meteor/app/api/server/v1/users.ts b/apps/meteor/app/api/server/v1/users.ts index f9c8feed16ba..ba8175f67412 100644 --- a/apps/meteor/app/api/server/v1/users.ts +++ b/apps/meteor/app/api/server/v1/users.ts @@ -598,6 +598,7 @@ API.v1.addRoute( { authRequired: true, validateParams: isUsersSendWelcomeEmailProps, + permissionsRequired: ['send-mail'], }, { async post() { From 60876b84778e71c450a198d5bbea5edeeb3fd950 Mon Sep 17 00:00:00 2001 From: rique223 Date: Thu, 14 Mar 2024 15:08:27 -0300 Subject: [PATCH 08/15] Review --- apps/meteor/app/api/server/lib/users.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/meteor/app/api/server/lib/users.ts b/apps/meteor/app/api/server/lib/users.ts index 22f18495b6d0..3ab174fd28e5 100644 --- a/apps/meteor/app/api/server/lib/users.ts +++ b/apps/meteor/app/api/server/lib/users.ts @@ -120,7 +120,7 @@ export function getNonEmptyQuery(query: Mongo.Query | undefi return { ...defaultQuery, ...query }; } -type findPaginatedUsersByStatusProps = { +type FindPaginatedUsersByStatusProps = { uid: string; offset: number; count: number; @@ -130,7 +130,7 @@ type findPaginatedUsersByStatusProps = { searchTerm: string; }; -export async function findPaginatedUsersByStatus({ uid, offset, count, sort, status, roles, searchTerm }: findPaginatedUsersByStatusProps) { +export async function findPaginatedUsersByStatus({ uid, offset, count, sort, status, roles, searchTerm }: FindPaginatedUsersByStatusProps) { const projection = { name: 1, username: 1, From 3b91f00a8301de47689d4555e0f34b131c96e19c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrique=20Guimar=C3=A3es=20Ribeiro?= Date: Thu, 14 Mar 2024 15:17:19 -0300 Subject: [PATCH 09/15] Create fifty-cups-sort.md --- .changeset/fifty-cups-sort.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/fifty-cups-sort.md diff --git a/.changeset/fifty-cups-sort.md b/.changeset/fifty-cups-sort.md new file mode 100644 index 000000000000..8f3e4512f055 --- /dev/null +++ b/.changeset/fifty-cups-sort.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/meteor": minor +"@rocket.chat/rest-typings": patch +--- + +Created a new endpoint to get a filtered and paginated list of users. From a6d49b2c1e8d6d647f600cf24d818668254b19e3 Mon Sep 17 00:00:00 2001 From: rique223 Date: Tue, 19 Mar 2024 18:43:56 -0300 Subject: [PATCH 10/15] Review --- .../client/views/admin/users/UsersTable/UsersTableRow.tsx | 4 ++-- packages/rest-typings/src/v1/users.ts | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/meteor/client/views/admin/users/UsersTable/UsersTableRow.tsx b/apps/meteor/client/views/admin/users/UsersTable/UsersTableRow.tsx index b7460123af7d..e0d46c2ba6fb 100644 --- a/apps/meteor/client/views/admin/users/UsersTable/UsersTableRow.tsx +++ b/apps/meteor/client/views/admin/users/UsersTable/UsersTableRow.tsx @@ -1,6 +1,6 @@ import type { IRole, IUser, Serialized } from '@rocket.chat/core-typings'; import { Box } from '@rocket.chat/fuselage'; -import type { PickedUser } from '@rocket.chat/rest-typings'; +import type { DefaultUserInfo } from '@rocket.chat/rest-typings'; import { capitalize } from '@rocket.chat/string-helpers'; import { UserAvatar } from '@rocket.chat/ui-avatar'; import type { TranslationKey } from '@rocket.chat/ui-contexts'; @@ -12,7 +12,7 @@ import { Roles } from '../../../../../app/models/client'; import { GenericTableRow, GenericTableCell } from '../../../../components/GenericTable'; type UsersTableRowProps = { - user: Serialized; + user: Serialized; onClick: (id: IUser['_id']) => void; mediaQuery: boolean; }; diff --git a/packages/rest-typings/src/v1/users.ts b/packages/rest-typings/src/v1/users.ts index eab2b421a14f..bcf99f951664 100644 --- a/packages/rest-typings/src/v1/users.ts +++ b/packages/rest-typings/src/v1/users.ts @@ -112,7 +112,7 @@ export type UserPresence = Readonly< export type UserPersonalTokens = Pick & { createdAt: string }; -export type PickedUser = Pick< +export type DefaultUserInfo = Pick< IUser, '_id' | 'username' | 'name' | 'status' | 'roles' | 'emails' | 'active' | 'avatarETag' | 'lastLogin' | 'type' >; @@ -146,18 +146,18 @@ export type UsersEndpoints = { '/v1/users.list': { GET: (params: PaginatedRequest<{ fields: string }>) => PaginatedResult<{ - users: PickedUser[]; + users: DefaultUserInfo[]; }>; }; '/v1/users.listByStatus': { GET: (params: UsersListStatusParamsGET) => PaginatedResult<{ - users: PickedUser[]; + users: DefaultUserInfo[]; }>; }; '/v1/users.sendWelcomeEmail': { - POST: (params: UsersSendWelcomeEmailParamsPOST) => { success: boolean }; + POST: (params: UsersSendWelcomeEmailParamsPOST) => void; }; '/v1/users.setAvatar': { From 1010637008f034db178e82ae224ab4bce794c3a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrique=20Guimar=C3=A3es=20Ribeiro?= Date: Thu, 21 Mar 2024 15:04:50 -0300 Subject: [PATCH 11/15] Create pink-parrots-end.md --- .changeset/pink-parrots-end.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/pink-parrots-end.md diff --git a/.changeset/pink-parrots-end.md b/.changeset/pink-parrots-end.md new file mode 100644 index 000000000000..133c7aec1350 --- /dev/null +++ b/.changeset/pink-parrots-end.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/meteor": minor +"@rocket.chat/rest-typings": patch +--- + +Created a new endpoint to resend the welcome email to a given user From 19a12b45205e4528640b7ce3195494b1eecd05c1 Mon Sep 17 00:00:00 2001 From: rique223 Date: Fri, 22 Mar 2024 16:51:41 -0300 Subject: [PATCH 12/15] =?UTF-8?q?refactor:=20=E2=99=BB=EF=B8=8F=20Refactor?= =?UTF-8?q?=20listUsersByStatus=20endpoint=20to=20be=20more=20flexible=20a?= =?UTF-8?q?nd=20update=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/meteor/app/api/server/lib/users.ts | 43 +++++++++++--------- apps/meteor/app/api/server/v1/users.ts | 4 +- apps/meteor/tests/end-to-end/api/01-users.js | 28 +++---------- 3 files changed, 31 insertions(+), 44 deletions(-) diff --git a/apps/meteor/app/api/server/lib/users.ts b/apps/meteor/app/api/server/lib/users.ts index 3ab174fd28e5..f80d662771df 100644 --- a/apps/meteor/app/api/server/lib/users.ts +++ b/apps/meteor/app/api/server/lib/users.ts @@ -125,12 +125,24 @@ type FindPaginatedUsersByStatusProps = { offset: number; count: number; sort: Record; - status: 'active' | 'all' | 'deactivated' | 'pending'; + status: 'active' | 'deactivated'; roles: string[] | null; searchTerm: string; + hasLoggedIn: boolean; + type: string; }; -export async function findPaginatedUsersByStatus({ uid, offset, count, sort, status, roles, searchTerm }: FindPaginatedUsersByStatusProps) { +export async function findPaginatedUsersByStatus({ + uid, + offset, + count, + sort, + status, + roles, + searchTerm, + hasLoggedIn, + type, +}: FindPaginatedUsersByStatusProps) { const projection = { name: 1, username: 1, @@ -141,39 +153,34 @@ export async function findPaginatedUsersByStatus({ uid, offset, count, sort, sta avatarETag: 1, lastLogin: 1, type: 1, - reason: 0, + reason: 1, }; const actualSort: Record = sort || { username: 1 }; - if (sort?.status) { actualSort.active = sort.status; } - if (sort?.name) { actualSort.nameInsensitive = sort.name; } - const match: Filter> = {}; - switch (status) { case 'active': match.active = true; - match.lastLogin = { $exists: true }; - break; - case 'all': break; case 'deactivated': match.active = false; - match.lastLogin = { $exists: true }; - break; - case 'pending': - match.lastLogin = { $exists: false }; - match.type = { $nin: ['bot', 'app'] }; - projection.reason = 1; break; } + if (hasLoggedIn !== undefined) { + match.lastLogin = { $exists: hasLoggedIn }; + } + + if (type) { + match.type = type; + } + const canSeeAllUserInfo = await hasPermissionAsync(uid, 'view-full-other-user-info'); match.$or = [ @@ -185,11 +192,9 @@ export async function findPaginatedUsersByStatus({ uid, offset, count, sort, sta name: { $regex: escapeRegExp(searchTerm || ''), $options: 'i' }, }, ]; - if (roles?.length && !roles.includes('all')) { match.roles = { $in: roles }; } - const { cursor, totalCount } = await Users.findPaginated( { ...match, @@ -201,9 +206,7 @@ export async function findPaginatedUsersByStatus({ uid, offset, count, sort, sta projection, }, ); - const [users, total] = await Promise.all([cursor.toArray(), totalCount]); - return { users, count: users.length, diff --git a/apps/meteor/app/api/server/v1/users.ts b/apps/meteor/app/api/server/v1/users.ts index ba8175f67412..2b85adf74169 100644 --- a/apps/meteor/app/api/server/v1/users.ts +++ b/apps/meteor/app/api/server/v1/users.ts @@ -576,7 +576,7 @@ API.v1.addRoute( const { offset, count } = await getPaginationItems(this.queryParams); const { sort } = await this.parseJsonQuery(); - const { status, roles, searchTerm } = this.queryParams; + const { status, hasLoggedIn, type, roles, searchTerm } = this.queryParams; return API.v1.success( await findPaginatedUsersByStatus({ @@ -587,6 +587,8 @@ API.v1.addRoute( status, roles, searchTerm, + hasLoggedIn, + type, }), ); }, 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 64a6b2f825e9..2d3f3a1d14ec 100644 --- a/apps/meteor/tests/end-to-end/api/01-users.js +++ b/apps/meteor/tests/end-to-end/api/01-users.js @@ -4500,76 +4500,63 @@ describe('[Users]', function () { await request .get(api('users.listByStatus')) .set(credentials) - .query({ status: 'pending', count: 50 }) + .query({ hasLoggedIn: false, type: 'user', count: 50 }) .expect('Content-Type', 'application/json') .expect(200) .expect((res) => { expect(res.body).to.have.property('success', true); expect(res.body).to.have.property('users'); - const { users } = res.body; const ids = users.map((user) => user._id); - expect(ids).to.include(user._id); }); }); it('should list all users', async () => { - await login(user.username, password); - await request .get(api('users.listByStatus')) .set(credentials) - .query({ status: 'all' }) .expect('Content-Type', 'application/json') .expect(200) .expect((res) => { expect(res.body).to.have.property('success', true); expect(res.body).to.have.property('users'); - const { users } = res.body; const ids = users.map((user) => user._id); - expect(ids).to.include(user._id); }); }); it('should list active users', async () => { await login(user.username, password); - await request .get(api('users.listByStatus')) .set(credentials) - .query({ status: 'active' }) + .query({ hasLoggedIn: true, status: 'active' }) .expect('Content-Type', 'application/json') .expect(200) .expect((res) => { expect(res.body).to.have.property('success', true); expect(res.body).to.have.property('users'); - const { users } = res.body; const ids = users.map((user) => user._id); - expect(ids).to.include(user._id); }); }); it('should filter users by role', async () => { await login(user.username, password); - await request .get(api('users.listByStatus')) .set(credentials) - .query({ 'status': 'active', 'roles[]': 'admin' }) + .query({ 'roles[]': 'admin' }) .expect('Content-Type', 'application/json') .expect(200) .expect((res) => { expect(res.body).to.have.property('success', true); expect(res.body).to.have.property('users'); - const { users } = res.body; const ids = users.map((user) => user._id); - expect(ids).to.not.include(user._id); }); }); @@ -4580,20 +4567,17 @@ describe('[Users]', function () { activeStatus: false, confirmRelinquish: false, }); - await request .get(api('users.listByStatus')) .set(credentials) - .query({ status: 'deactivated' }) + .query({ hasLoggedIn: true, status: 'deactivated' }) .expect('Content-Type', 'application/json') .expect(200) .expect((res) => { expect(res.body).to.have.property('success', true); expect(res.body).to.have.property('users'); - const { users } = res.body; const ids = users.map((user) => user._id); - expect(ids).to.include(user._id); }); }); @@ -4602,16 +4586,14 @@ describe('[Users]', function () { await request .get(api('users.listByStatus')) .set(credentials) - .query({ status: 'all', searchTerm: user.username }) + .query({ searchTerm: user.username }) .expect('Content-Type', 'application/json') .expect(200) .expect((res) => { expect(res.body).to.have.property('success', true); expect(res.body).to.have.property('users'); - const { users } = res.body; const ids = users.map((user) => user._id); - expect(ids).to.include(user._id); }); }); From 8923dcaeb2514cf8f5f5fa3c9a0c528cec31acfb Mon Sep 17 00:00:00 2001 From: rique223 Date: Fri, 22 Mar 2024 17:19:12 -0300 Subject: [PATCH 13/15] Update listByStatus typing --- .../src/v1/users/UsersListStatusParamsGET.ts | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/packages/rest-typings/src/v1/users/UsersListStatusParamsGET.ts b/packages/rest-typings/src/v1/users/UsersListStatusParamsGET.ts index a991790911be..25024aee12cb 100644 --- a/packages/rest-typings/src/v1/users/UsersListStatusParamsGET.ts +++ b/packages/rest-typings/src/v1/users/UsersListStatusParamsGET.ts @@ -7,17 +7,26 @@ const ajv = new Ajv({ }); export type UsersListStatusParamsGET = PaginatedRequest<{ - status: 'active' | 'all' | 'deactivated' | 'pending'; + status?: 'active' | 'deactivated'; + hasLoggedIn?: boolean; + type?: string; roles?: string[]; searchTerm?: string; }>; - const UsersListStatusParamsGetSchema = { type: 'object', properties: { status: { type: 'string', - enum: ['active', 'all', 'deactivated', 'pending'], + enum: ['active', 'deactivated'], + }, + hasLoggedIn: { + type: 'boolean', + nullable: true, + }, + type: { + type: 'string', + nullable: true, }, roles: { type: 'array', @@ -43,7 +52,6 @@ const UsersListStatusParamsGetSchema = { nullable: true, }, }, - required: ['status'], additionalProperties: false, }; From fbb307b7a0ba73301e11911e6638408d90d6da1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrique=20Guimar=C3=A3es=20Ribeiro?= Date: Mon, 1 Apr 2024 16:44:55 -0300 Subject: [PATCH 14/15] Update fifty-cups-sort.md --- .changeset/fifty-cups-sort.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/fifty-cups-sort.md b/.changeset/fifty-cups-sort.md index 8f3e4512f055..389391ef8cc9 100644 --- a/.changeset/fifty-cups-sort.md +++ b/.changeset/fifty-cups-sort.md @@ -1,6 +1,6 @@ --- "@rocket.chat/meteor": minor -"@rocket.chat/rest-typings": patch +"@rocket.chat/rest-typings": minor --- Created a new endpoint to get a filtered and paginated list of users. From afd510840c81b58b3b2ecadfc5d150e8f9fcbc83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrique=20Guimar=C3=A3es=20Ribeiro?= Date: Mon, 1 Apr 2024 16:45:24 -0300 Subject: [PATCH 15/15] Update pink-parrots-end.md --- .changeset/pink-parrots-end.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/pink-parrots-end.md b/.changeset/pink-parrots-end.md index 133c7aec1350..9f1863f6915c 100644 --- a/.changeset/pink-parrots-end.md +++ b/.changeset/pink-parrots-end.md @@ -1,6 +1,6 @@ --- "@rocket.chat/meteor": minor -"@rocket.chat/rest-typings": patch +"@rocket.chat/rest-typings": minor --- Created a new endpoint to resend the welcome email to a given user