diff --git a/.changeset/flat-stingrays-complain.md b/.changeset/flat-stingrays-complain.md new file mode 100644 index 0000000000000..627bb0f9b333a --- /dev/null +++ b/.changeset/flat-stingrays-complain.md @@ -0,0 +1,6 @@ +--- +'@rocket.chat/rest-typings': minor +'@rocket.chat/meteor': minor +--- + +Migration of groups.memebers added ajv validation for request and response, schema changes for request. diff --git a/apps/meteor/app/api/server/v1/groups.ts b/apps/meteor/app/api/server/v1/groups.ts index dba48f1a9ffa9..d9b9f0e897996 100644 --- a/apps/meteor/app/api/server/v1/groups.ts +++ b/apps/meteor/app/api/server/v1/groups.ts @@ -1,7 +1,17 @@ import { Team, isMeteorError } from '@rocket.chat/core-services'; import type { IIntegration, IUser, IRoom, RoomType, UserStatus } from '@rocket.chat/core-typings'; import { Integrations, Messages, Rooms, Subscriptions, Uploads, Users } from '@rocket.chat/models'; -import { isGroupsOnlineProps, isGroupsMessagesProps, isGroupsFilesProps } from '@rocket.chat/rest-typings'; +import { + isGroupsOnlineProps, + isGroupsMessagesProps, + isGroupsFilesProps, + ajv, + validateBadRequestErrorResponse, + validateUnauthorizedErrorResponse, + validateForbiddenErrorResponse, + withGroupBaseProperties, +} from '@rocket.chat/rest-typings'; +import type { PaginatedRequest, GroupsBaseProps } from '@rocket.chat/rest-typings'; import { isTruthy } from '@rocket.chat/tools'; import { check, Match } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; @@ -31,12 +41,112 @@ import { executeGetRoomRoles } from '../../../lib/server/methods/getRoomRoles'; import { leaveRoomMethod } from '../../../lib/server/methods/leaveRoom'; import { executeUnarchiveRoom } from '../../../lib/server/methods/unarchiveRoom'; import { normalizeMessagesForUser } from '../../../utils/server/lib/normalizeMessagesForUser'; +import type { ExtractRoutesFromAPI } from '../ApiClass'; import { API } from '../api'; import { addUserToFileObj } from '../helpers/addUserToFileObj'; import { composeRoomWithLastMessage } from '../helpers/composeRoomWithLastMessage'; import { getPaginationItems } from '../helpers/getPaginationItems'; import { getUserFromParams, getUserListFromParams } from '../helpers/getUserFromParams'; +type GroupsMembersProps = PaginatedRequest; + +const GroupsMembersPropsSchema = withGroupBaseProperties({ + offset: { + type: 'string', + }, + count: { + type: 'string', + }, + filter: { + type: 'string', + }, + query: { + type: 'string', + nullable: true, + }, + sort: { + type: 'string', + nullable: true, + }, + status: { + type: 'array', + items: { type: 'string' }, + }, +}); + +const isGroupsMembersProps = ajv.compile(GroupsMembersPropsSchema); + +const isGroupsMembersResponse = ajv.compile({ + type: 'object', + properties: { + members: { + type: 'array', + items: { $ref: '#/components/schemas/IUser' }, + }, + count: { type: 'integer' }, + offset: { type: 'integer' }, + total: { type: 'integer' }, + success: { type: 'boolean', enum: [true] }, + }, + required: ['members', 'success','count','offset','total'], + additionalProperties: false, +}); + +const groupsEndPoints = API.v1.get( + 'groups.members', + { + authRequired: true, + query: isGroupsMembersProps, + response: { + 200: isGroupsMembersResponse, + 400: validateBadRequestErrorResponse, + 401: validateUnauthorizedErrorResponse, + 403: validateForbiddenErrorResponse, + }, + }, + async function action() { + const findResult = await findPrivateGroupByIdOrName({ + params: this.queryParams, + userId: this.userId, + }); + + if (findResult.broadcast && !(await hasPermissionAsync(this.userId, 'view-broadcast-member-list', findResult.rid))) { + return API.v1.forbidden('User does not have the permissions required for this action'); + } + + const { offset: skip, count: limit } = await getPaginationItems(this.queryParams); + const { sort = {} } = await this.parseJsonQuery(); + + check( + this.queryParams, + Match.ObjectIncluding({ + status: Match.Maybe([String]), + filter: Match.Maybe(String), + }), + ); + + const { status, filter } = this.queryParams; + + const { cursor, totalCount } = await findUsersOfRoom({ + rid: findResult.rid, + ...(status && { status: { $in: status as UserStatus[] } }), + skip, + limit, + filter, + ...(sort?.username && { sort: { username: sort.username } }), + }); + + const [members, total] = await Promise.all([cursor.toArray(), totalCount]); + + return API.v1.success({ + members, + count: members.length, + offset: skip, + total, + }); + }, +); + async function getRoomFromParams(params: { roomId?: string } | { roomName?: string }): Promise { if ( (!('roomId' in params) && !('roomName' in params)) || @@ -718,54 +828,6 @@ API.v1.addRoute( }, ); -API.v1.addRoute( - 'groups.members', - { authRequired: true }, - { - async get() { - const findResult = await findPrivateGroupByIdOrName({ - params: this.queryParams, - userId: this.userId, - }); - - if (findResult.broadcast && !(await hasPermissionAsync(this.userId, 'view-broadcast-member-list', findResult.rid))) { - return API.v1.forbidden(); - } - - const { offset: skip, count: limit } = await getPaginationItems(this.queryParams); - const { sort = {} } = await this.parseJsonQuery(); - - check( - this.queryParams, - Match.ObjectIncluding({ - status: Match.Maybe([String]), - filter: Match.Maybe(String), - }), - ); - - const { status, filter } = this.queryParams; - - const { cursor, totalCount } = await findUsersOfRoom({ - rid: findResult.rid, - ...(status && { status: { $in: status as UserStatus[] } }), - skip, - limit, - filter, - ...(sort?.username && { sort: { username: sort.username } }), - }); - - const [members, total] = await Promise.all([cursor.toArray(), totalCount]); - - return API.v1.success({ - members, - count: members.length, - offset: skip, - total, - }); - }, - }, -); - API.v1.addRoute( 'groups.messages', { authRequired: true, validateParams: isGroupsMessagesProps }, @@ -1300,3 +1362,10 @@ API.v1.addRoute( }, }, ); + +export type GroupsHistoryEndpoints = ExtractRoutesFromAPI; + +declare module '@rocket.chat/rest-typings' { + // eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/no-empty-interface + interface Endpoints extends GroupsHistoryEndpoints {} +} diff --git a/packages/rest-typings/src/v1/groups/GroupsMembersProps.ts b/packages/rest-typings/src/v1/groups/GroupsMembersProps.ts deleted file mode 100644 index 6540b2d0d174c..0000000000000 --- a/packages/rest-typings/src/v1/groups/GroupsMembersProps.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { ajv } from '../Ajv'; -import type { GroupsBaseProps } from './BaseProps'; -import { withGroupBaseProperties } from './BaseProps'; -import type { PaginatedRequest } from '../../helpers/PaginatedRequest'; - -export type GroupsMembersProps = PaginatedRequest; - -const GroupsMembersPropsSchema = withGroupBaseProperties({ - offset: { - type: 'number', - nullable: true, - }, - count: { - type: 'number', - nullable: true, - }, - filter: { - type: 'string', - nullable: true, - }, - status: { - type: 'array', - items: { type: 'string' }, - nullable: true, - }, -}); - -export const isGroupsMembersProps = ajv.compile(GroupsMembersPropsSchema); diff --git a/packages/rest-typings/src/v1/groups/groups.ts b/packages/rest-typings/src/v1/groups/groups.ts index a7c937b08a9fc..9c797dfd85cf8 100644 --- a/packages/rest-typings/src/v1/groups/groups.ts +++ b/packages/rest-typings/src/v1/groups/groups.ts @@ -18,7 +18,6 @@ import type { GroupsInviteProps } from './GroupsInviteProps'; import type { GroupsKickProps } from './GroupsKickProps'; import type { GroupsLeaveProps } from './GroupsLeaveProps'; import type { GroupsListProps } from './GroupsListProps'; -import type { GroupsMembersProps } from './GroupsMembersProps'; import type { GroupsMessagesProps } from './GroupsMessagesProps'; import type { GroupsModeratorsProps } from './GroupsModeratorsProps'; import type { GroupsOnlineProps } from './GroupsOnlineProps'; @@ -46,14 +45,6 @@ export type GroupsEndpoints = { files: IUploadWithUser[]; }>; }; - '/v1/groups.members': { - GET: (params: GroupsMembersProps) => { - count: number; - offset: number; - members: IUser[]; - total: number; - }; - }; '/v1/groups.history': { GET: (params: GroupsHistoryProps) => PaginatedResult<{ messages: IMessage[]; diff --git a/packages/rest-typings/src/v1/groups/index.ts b/packages/rest-typings/src/v1/groups/index.ts index aad463d285c9d..c6f4581f36640 100644 --- a/packages/rest-typings/src/v1/groups/index.ts +++ b/packages/rest-typings/src/v1/groups/index.ts @@ -1,5 +1,6 @@ export type * from './groups'; +export * from './BaseProps'; export * from './GroupsArchiveProps'; export * from './GroupsCloseProps'; export * from './GroupsConvertToTeamProps'; @@ -9,7 +10,6 @@ export * from './GroupsDeleteProps'; export * from './GroupsFilesProps'; export * from './GroupsKickProps'; export * from './GroupsLeaveProps'; -export * from './GroupsMembersProps'; export * from './GroupsMessagesProps'; export * from './GroupsRolesProps'; export * from './GroupsUnarchiveProps';