diff --git a/apps/meteor/app/api/server/helpers/parseJsonQuery.ts b/apps/meteor/app/api/server/helpers/parseJsonQuery.ts index 9c088c8f31e1fe0184d51b00e71089e153e29a37..7818f0d97fa2edeff250683482ccb007a7fe2e79 100644 --- a/apps/meteor/app/api/server/helpers/parseJsonQuery.ts +++ b/apps/meteor/app/api/server/helpers/parseJsonQuery.ts @@ -54,7 +54,7 @@ export async function parseJsonQuery(api: PartialThis): Promise<{ } // TODO: Remove this once we have all routes migrated to the new API params - const hasSupportedRoutes = ([] as string[]).includes(route); + const hasSupportedRoutes = ['/api/v1/custom-user-status.list'].includes(route); const isUnsafeQueryParamsAllowed = process.env.ALLOW_UNSAFE_QUERY_AND_FIELDS_API_PARAMS?.toUpperCase() === 'TRUE'; const messageGenerator = ({ endpoint, version, parameter }: { endpoint: string; version: string; parameter: string }): string => `The usage of the "${parameter}" parameter in endpoint "${endpoint}" breaks the security of the API and can lead to data exposure. It has been deprecated and will be removed in the version ${version}.`; diff --git a/apps/meteor/app/api/server/v1/custom-user-status.ts b/apps/meteor/app/api/server/v1/custom-user-status.ts index d665313b98e674fb11e9bbe08b9b11ff922065b7..4f17c0db1ae8944bb13a806024726fe48770d840 100644 --- a/apps/meteor/app/api/server/v1/custom-user-status.ts +++ b/apps/meteor/app/api/server/v1/custom-user-status.ts @@ -1,4 +1,6 @@ import { CustomUserStatus } from '@rocket.chat/models'; +import { isCustomUserStatusListProps } from '@rocket.chat/rest-typings'; +import { escapeRegExp } from '@rocket.chat/string-helpers'; import { Match, check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; @@ -7,13 +9,21 @@ import { getPaginationItems } from '../helpers/getPaginationItems'; API.v1.addRoute( 'custom-user-status.list', - { authRequired: true }, + { authRequired: true, validateParams: isCustomUserStatusListProps }, { async get() { - const { offset, count } = await getPaginationItems(this.queryParams); + const { offset, count } = await getPaginationItems(this.queryParams as Record<string, string | number | null | undefined>); const { sort, query } = await this.parseJsonQuery(); - const { cursor, totalCount } = CustomUserStatus.findPaginated(query, { + const { name, _id } = this.queryParams; + + const filter = { + ...query, + ...(name ? { name: { $regex: escapeRegExp(name as string), $options: 'i' } } : {}), + ...(_id ? { _id } : {}), + }; + + const { cursor, totalCount } = CustomUserStatus.findPaginated(filter, { sort: sort || { name: 1 }, skip: offset, limit: count, diff --git a/apps/meteor/client/views/admin/customUserStatus/CustomUserStatusFormWithData.tsx b/apps/meteor/client/views/admin/customUserStatus/CustomUserStatusFormWithData.tsx index d8d552a4341cb2c750665bd448a514ed61ebcf39..101de668e6a1e2241ff0f0263c7e58bc92fa2cf5 100644 --- a/apps/meteor/client/views/admin/customUserStatus/CustomUserStatusFormWithData.tsx +++ b/apps/meteor/client/views/admin/customUserStatus/CustomUserStatusFormWithData.tsx @@ -16,7 +16,7 @@ type CustomUserStatusFormWithDataProps = { const CustomUserStatusFormWithData = ({ _id, onReload, onClose }: CustomUserStatusFormWithDataProps): ReactElement => { const t = useTranslation(); - const query = useMemo(() => ({ query: JSON.stringify({ _id }) }), [_id]); + const query = useMemo(() => ({ _id }), [_id]); const getCustomUserStatus = useEndpoint('GET', '/v1/custom-user-status.list'); diff --git a/apps/meteor/client/views/admin/customUserStatus/CustomUserStatusTable/CustomUserStatusTable.tsx b/apps/meteor/client/views/admin/customUserStatus/CustomUserStatusTable/CustomUserStatusTable.tsx index d18fc4a706a8be7d011ff86169e579e885ff1dff..0aa24542ca96b12d7fe58a4498965554c58e73ea 100644 --- a/apps/meteor/client/views/admin/customUserStatus/CustomUserStatusTable/CustomUserStatusTable.tsx +++ b/apps/meteor/client/views/admin/customUserStatus/CustomUserStatusTable/CustomUserStatusTable.tsx @@ -34,7 +34,7 @@ const CustomUserStatus = ({ reload, onClick }: CustomUserStatusProps): ReactElem const query = useDebouncedValue( useMemo( () => ({ - query: JSON.stringify({ name: { $regex: escapeRegExp(text), $options: 'i' } }), + name: escapeRegExp(text), sort: `{ "${sortBy}": ${sortDirection === 'asc' ? 1 : -1} }`, count: itemsPerPage, offset: current, diff --git a/apps/meteor/tests/end-to-end/api/custom-user-status.ts b/apps/meteor/tests/end-to-end/api/custom-user-status.ts index c1ecd185dae83d037536b7a6aa6c7827030328c8..d157d79befa9777d9b83875f216d2289ca184b17 100644 --- a/apps/meteor/tests/end-to-end/api/custom-user-status.ts +++ b/apps/meteor/tests/end-to-end/api/custom-user-status.ts @@ -1,12 +1,35 @@ import { expect } from 'chai'; -import { before, describe, it } from 'mocha'; +import { after, before, describe, it } from 'mocha'; import { getCredentials, api, request, credentials } from '../../data/api-data'; +async function createCustomUserStatus(name: string): Promise<string> { + const res = await request.post(api('custom-user-status.create')).set(credentials).send({ name }).expect(200); + return res.body.customUserStatus._id; +} + +async function deleteCustomUserStatus(id: string): Promise<void> { + await request.post(api('custom-user-status.delete')).set(credentials).send({ customUserStatusId: id }).expect(200); +} + describe('[CustomUserStatus]', () => { - before((done) => getCredentials(done)); + before((done) => { + getCredentials(done); + }); describe('[/custom-user-status.list]', () => { + let customUserStatusId: string; + let customUserStatusName: string; + + before(async () => { + customUserStatusName = `test-${Date.now()}`; + customUserStatusId = await createCustomUserStatus(customUserStatusName); + }); + + after(async () => { + await deleteCustomUserStatus(customUserStatusId); + }); + it('should return custom user status', (done) => { void request .get(api('custom-user-status.list')) @@ -20,6 +43,7 @@ describe('[CustomUserStatus]', () => { }) .end(done); }); + it('should return custom user status even requested with count and offset params', (done) => { void request .get(api('custom-user-status.list')) @@ -37,5 +61,56 @@ describe('[CustomUserStatus]', () => { }) .end(done); }); + + it('should return one custom user status when requested with id param', (done) => { + void request + .get(api('custom-user-status.list')) + .set(credentials) + .expect(200) + .query({ + _id: customUserStatusId, + }) + .expect((res) => { + expect(res.body).to.have.property('statuses').and.to.be.an('array').and.to.have.lengthOf(1); + expect(res.body).to.have.property('total').and.to.equal(1); + expect(res.body).to.have.property('offset').and.to.equal(0); + expect(res.body).to.have.property('count').and.to.equal(1); + }) + .end(done); + }); + + it('should return empty array when requested with an existing name param', (done) => { + void request + .get(api('custom-user-status.list')) + .set(credentials) + .expect(200) + .query({ + name: customUserStatusName, + }) + .expect((res) => { + expect(res.body).to.have.property('statuses').and.to.be.an('array').and.to.have.lengthOf(1); + expect(res.body).to.have.property('total').and.to.equal(1); + expect(res.body).to.have.property('offset').and.to.equal(0); + expect(res.body).to.have.property('count').and.to.equal(1); + }) + .end(done); + }); + + it('should return empty array when requested with unknown name param', (done) => { + void request + .get(api('custom-user-status.list')) + .set(credentials) + .expect(200) + .query({ + name: 'testxxx', + }) + .expect((res) => { + expect(res.body).to.have.property('statuses').and.to.be.an('array').and.to.have.lengthOf(0); + expect(res.body).to.have.property('total').and.to.equal(0); + expect(res.body).to.have.property('offset').and.to.equal(0); + expect(res.body).to.have.property('count').and.to.equal(0); + }) + .end(done); + }); }); }); diff --git a/packages/rest-typings/src/index.ts b/packages/rest-typings/src/index.ts index 36c4d19b8ae34db2c9725389c217bd33de14c69f..1568001f3aa8fdab3df60192759304880202d19b 100644 --- a/packages/rest-typings/src/index.ts +++ b/packages/rest-typings/src/index.ts @@ -221,6 +221,7 @@ export * from './v1/teams'; export * from './v1/videoConference'; export * from './v1/assets'; export * from './v1/channels'; +export * from './v1/customUserStatus'; export * from './v1/subscriptionsEndpoints'; export * from './v1/mailer'; export * from './v1/mailer/MailerParamsPOST'; diff --git a/packages/rest-typings/src/v1/customUserStatus.ts b/packages/rest-typings/src/v1/customUserStatus.ts index a6131abfca8ddde4e5c1a1f7263056091618e1f4..495079d5e73fcbe6f41106d92028df092bd575c2 100644 --- a/packages/rest-typings/src/v1/customUserStatus.ts +++ b/packages/rest-typings/src/v1/customUserStatus.ts @@ -8,7 +8,7 @@ const ajv = new Ajv({ coerceTypes: true, }); -type CustomUserStatusListProps = PaginatedRequest<{ query: string }>; +type CustomUserStatusListProps = PaginatedRequest<{ name?: string; _id?: string; query?: string }>; const CustomUserStatusListSchema = { type: 'object', @@ -25,11 +25,20 @@ const CustomUserStatusListSchema = { type: 'string', nullable: true, }, + name: { + type: 'string', + nullable: true, + }, + _id: { + type: 'string', + nullable: true, + }, query: { type: 'string', + nullable: true, }, }, - required: ['query'], + required: [], additionalProperties: false, };