diff --git a/apps/meteor/app/api/server/helpers/parseJsonQuery.ts b/apps/meteor/app/api/server/helpers/parseJsonQuery.ts index 7818f0d97fa2..cfd62125a023 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 = ['/api/v1/custom-user-status.list'].includes(route); + const hasSupportedRoutes = ['/api/v1/integrations.list', '/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/integrations.ts b/apps/meteor/app/api/server/v1/integrations.ts index 5306e7fbbea3..1c1dd9dd50f3 100644 --- a/apps/meteor/app/api/server/v1/integrations.ts +++ b/apps/meteor/app/api/server/v1/integrations.ts @@ -6,7 +6,9 @@ import { isIntegrationsRemoveProps, isIntegrationsGetProps, isIntegrationsUpdateProps, + isIntegrationsListProps, } from '@rocket.chat/rest-typings'; +import { escapeRegExp } from '@rocket.chat/string-helpers'; import { Match, check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; import type { Filter } from 'mongodb'; @@ -86,6 +88,7 @@ API.v1.addRoute( 'integrations.list', { authRequired: true, + validateParams: isIntegrationsListProps, permissionsRequired: { GET: { permissions: [ @@ -101,15 +104,22 @@ API.v1.addRoute( { async get() { const { offset, count } = await getPaginationItems(this.queryParams); - const { sort, fields: projection, query } = await this.parseJsonQuery(); + const { sort, fields, query } = await this.parseJsonQuery(); + const { name, type } = this.queryParams; + + const filter = { + ...query, + ...(name ? { name: { $regex: escapeRegExp(name as string), $options: 'i' } } : {}), + ...(type ? { type } : {}), + }; - const ourQuery = Object.assign(await mountIntegrationQueryBasedOnPermissions(this.userId), query) as Filter; + const ourQuery = Object.assign(await mountIntegrationQueryBasedOnPermissions(this.userId), filter) as Filter; const { cursor, totalCount } = Integrations.findPaginated(ourQuery, { sort: sort || { ts: -1 }, skip: offset, limit: count, - projection, + projection: fields, }); const [integrations, total] = await Promise.all([cursor.toArray(), totalCount]); diff --git a/apps/meteor/client/views/admin/integrations/IntegrationsTable.tsx b/apps/meteor/client/views/admin/integrations/IntegrationsTable.tsx index 2fd828f1507c..791b5a6ad02e 100644 --- a/apps/meteor/client/views/admin/integrations/IntegrationsTable.tsx +++ b/apps/meteor/client/views/admin/integrations/IntegrationsTable.tsx @@ -30,7 +30,8 @@ const IntegrationsTable = ({ type }: { type?: string }) => { const query = useDebouncedValue( useMemo( () => ({ - query: JSON.stringify({ name: { $regex: escapeRegExp(text), $options: 'i' }, type }), + name: escapeRegExp(text), + type, sort: `{ "${sortBy}": ${sortDirection === 'asc' ? 1 : -1} }`, count: itemsPerPage, offset: current, diff --git a/packages/rest-typings/src/v1/integrations/IntegrationsListProps.ts b/packages/rest-typings/src/v1/integrations/IntegrationsListProps.ts new file mode 100644 index 000000000000..cf44d2409c96 --- /dev/null +++ b/packages/rest-typings/src/v1/integrations/IntegrationsListProps.ts @@ -0,0 +1,41 @@ +import Ajv from 'ajv'; + +import type { PaginatedRequest } from '../../helpers/PaginatedRequest'; + +const ajv = new Ajv(); + +export type IntegrationsListProps = PaginatedRequest<{ name?: string; type?: string; query?: string }>; + +const integrationsListSchema = { + type: 'object', + properties: { + count: { + type: ['number', 'string'], + nullable: true, + }, + offset: { + type: ['number', 'string'], + nullable: true, + }, + sort: { + type: 'string', + nullable: true, + }, + name: { + type: 'string', + nullable: true, + }, + type: { + type: 'string', + nullable: true, + }, + query: { + type: 'string', + nullable: true, + }, + }, + required: [], + additionalProperties: false, +}; + +export const isIntegrationsListProps = ajv.compile(integrationsListSchema); diff --git a/packages/rest-typings/src/v1/integrations/index.ts b/packages/rest-typings/src/v1/integrations/index.ts index 6de1aaa0bd67..e8fb458f74e6 100644 --- a/packages/rest-typings/src/v1/integrations/index.ts +++ b/packages/rest-typings/src/v1/integrations/index.ts @@ -5,3 +5,4 @@ export * from './IntegrationsHistoryProps'; export * from './IntegrationsRemoveProps'; export * from './IntegrationsGetProps'; export * from './IntegrationsUpdateProps'; +export * from './IntegrationsListProps'; diff --git a/packages/rest-typings/src/v1/integrations/integrations.ts b/packages/rest-typings/src/v1/integrations/integrations.ts index 238f1b830c50..4557937b5b77 100644 --- a/packages/rest-typings/src/v1/integrations/integrations.ts +++ b/packages/rest-typings/src/v1/integrations/integrations.ts @@ -1,10 +1,10 @@ import type { IIntegration, IIntegrationHistory } from '@rocket.chat/core-typings'; -import type { PaginatedRequest } from '../../helpers/PaginatedRequest'; import type { PaginatedResult } from '../../helpers/PaginatedResult'; import type { IntegrationsCreateProps } from './IntegrationsCreateProps'; import type { IntegrationsGetProps } from './IntegrationsGetProps'; import type { IntegrationsHistoryProps } from './IntegrationsHistoryProps'; +import type { IntegrationsListProps } from './IntegrationsListProps'; import type { IntegrationsRemoveProps } from './IntegrationsRemoveProps'; import type { IntegrationsUpdateProps } from './IntegrationsUpdateProps'; @@ -21,7 +21,7 @@ export type IntegrationsEndpoints = { }; '/v1/integrations.list': { - GET: (params: PaginatedRequest) => PaginatedResult<{ + GET: (params: IntegrationsListProps) => PaginatedResult<{ integrations: IIntegration[]; items: number; }>;