From 1286a3a08f1589eb7b6b010947e17aeacad8edda Mon Sep 17 00:00:00 2001 From: Ricardo Garim <rswarovsky@gmail.com> Date: Wed, 16 Oct 2024 14:26:55 -0300 Subject: [PATCH] chore!: make custom user status pages stop to use query param (#33557) --- .../app/api/server/helpers/parseJsonQuery.ts | 2 +- .../app/api/server/v1/custom-user-status.ts | 16 +++- .../CustomUserStatusFormWithData.tsx | 2 +- .../CustomUserStatusTable.tsx | 2 +- .../end-to-end/api/custom-user-status.ts | 79 ++++++++++++++++++- packages/rest-typings/src/index.ts | 1 + .../rest-typings/src/v1/customUserStatus.ts | 13 ++- 7 files changed, 105 insertions(+), 10 deletions(-) diff --git a/apps/meteor/app/api/server/helpers/parseJsonQuery.ts b/apps/meteor/app/api/server/helpers/parseJsonQuery.ts index 9c088c8f31e..7818f0d97fa 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 d665313b98e..4f17c0db1ae 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 d8d552a4341..101de668e6a 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 d18fc4a706a..0aa24542ca9 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 c1ecd185dae..d157d79befa 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 36c4d19b8ae..1568001f3aa 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 a6131abfca8..495079d5e73 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, }; -- GitLab