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