From fa5421c088b7186f4e640ac9381fed4954d3d01f Mon Sep 17 00:00:00 2001 From: harsheetsharma Date: Wed, 1 Apr 2026 07:17:34 +0000 Subject: [PATCH 1/2] chore: migrate rooms.cleanHistory endpoint to new API format with AJV validation --- apps/meteor/app/api/server/v1/rooms.ts | 167 +++++++++++++++---------- packages/rest-typings/src/v1/rooms.ts | 3 - 2 files changed, 104 insertions(+), 66 deletions(-) diff --git a/apps/meteor/app/api/server/v1/rooms.ts b/apps/meteor/app/api/server/v1/rooms.ts index bb4278bbb70fb..4d38fa09b8b9f 100644 --- a/apps/meteor/app/api/server/v1/rooms.ts +++ b/apps/meteor/app/api/server/v1/rooms.ts @@ -76,12 +76,12 @@ export async function findRoomByIdOrName({ checkedArchived = true, }: { params: - | { - roomId?: string; - } - | { - roomName?: string; - }; + | { + roomId?: string; + } + | { + roomName?: string; + }; checkedArchived?: boolean; }): Promise { if ( @@ -372,54 +372,94 @@ const roomsSaveNotificationEndpoint = API.v1.post( }, ); -API.v1.addRoute( - 'rooms.cleanHistory', - { authRequired: true, validateParams: isRoomsCleanHistoryProps }, +const roomsCleanHistoryEndpoint = API.v1.post('rooms.cleanHistory', { - async post() { - const room = await findRoomByIdOrName({ params: this.bodyParams }); - const { _id } = room; - - if (!room || !(await canAccessRoomAsync(room, { _id: this.userId }))) { - return API.v1.failure('User does not have access to the room [error-not-allowed]', 'error-not-allowed'); - } - - const { - latest, - oldest, - inclusive = false, - limit, - excludePinned, - filesOnly, - ignoreThreads, - ignoreDiscussion, - users, - } = this.bodyParams; - - if (!latest) { - return API.v1.failure('Body parameter "latest" is required.'); - } - - if (!oldest) { - return API.v1.failure('Body parameter "oldest" is required.'); - } - - const count = await cleanRoomHistoryMethod(this.userId, { - roomId: _id, - latest: new Date(latest), - oldest: new Date(oldest), - inclusive, - limit, - excludePinned: [true, 'true', 1, '1'].includes(excludePinned ?? false), - filesOnly: [true, 'true', 1, '1'].includes(filesOnly ?? false), - ignoreThreads: [true, 'true', 1, '1'].includes(ignoreThreads ?? false), - ignoreDiscussion: [true, 'true', 1, '1'].includes(ignoreDiscussion ?? false), - fromUsers: users?.filter(isTruthy) || [], - }); - - return API.v1.success({ _id, count }); + authRequired: true, + body: ajv.compile<{ + roomId?: string; + roomName?: string; + latest: string; + oldest: string; + inclusive?: boolean; + limit?: number; + excludePinned?: boolean | string | number; + filesOnly?: boolean | string | number; + ignoreThreads?: boolean | string | number; + ignoreDiscussion?: boolean | string | number; + users?: string[]; + + }>({ + type: 'object', + properties: { + roomId: { type: 'string' }, + roomName: { type: 'string' }, + latest: { type: 'string', description: 'The end date of the range' }, + oldest: { type: 'string', description: 'The start date of the range' }, + inclusive: { type: 'boolean' }, + limit: { type: 'number' }, + excludePinned: { type: ['boolean', 'string', 'number'] }, + filesOnly: { type: ['boolean', 'string', 'number'] }, + ignoreThreads: { type: ['boolean', 'string', 'number'] }, + ignoreDiscussion: { type: ['boolean', 'string', 'number'] }, + users: { type: 'array', items: { type: 'string' } } + }, + required: ['latest', 'oldest'], + additionalProperties: false + }), + response: { + 200: ajv.compile<{ + _id: string; + count: number; + success: true; + }>({ + type: 'object', + properties: { + _id: { type: 'string' }, + count: { type: 'number' }, + success: { type: 'boolean', enum: [true] }, + }, + required: ['_id', 'count', 'success'], + additionalProperties: false, + }), + 400: validateBadRequestErrorResponse, + 401: validateUnauthorizedErrorResponse, }, }, + async function action() { + const room = await findRoomByIdOrName({ params: this.bodyParams }); + + if (!room || !(await canAccessRoomAsync(room, { _id: this.userId }))) { + return API.v1.failure('User does not have access to the room [error-not-allowed]', 'error-not-allowed'); + } + const { _id } = room; + + const { + latest, + oldest, + inclusive = false, + limit, + excludePinned, + filesOnly, + ignoreThreads, + ignoreDiscussion, + users, + } = this.bodyParams; + + const count = await cleanRoomHistoryMethod(this.userId, { + roomId: _id, + latest: new Date(latest), + oldest: new Date(oldest), + inclusive, + limit, + excludePinned: [true, 'true', 1, '1'].includes(excludePinned ?? false), + filesOnly: [true, 'true', 1, '1'].includes(filesOnly ?? false), + ignoreThreads: [true, 'true', 1, '1'].includes(ignoreThreads ?? false), + ignoreDiscussion: [true, 'true', 1, '1'].includes(ignoreDiscussion ?? false), + fromUsers: users?.filter(isTruthy) || [], + }); + + return API.v1.success({ _id, count }); + } ); API.v1.addRoute( @@ -970,21 +1010,21 @@ API.v1.addRoute( type RoomsFavorite = | { - roomId: string; - favorite: boolean; - } + roomId: string; + favorite: boolean; + } | { - roomName: string; - favorite: boolean; - }; + roomName: string; + favorite: boolean; + }; type RoomsLeave = | { - roomId: string; - } + roomId: string; + } | { - roomName: string; - }; + roomName: string; + }; const isRoomGetRolesPropsSchema = { type: 'object', @@ -1377,9 +1417,10 @@ export const roomEndpoints = API.v1 ); type RoomEndpoints = ExtractRoutesFromAPI & ExtractRoutesFromAPI & - ExtractRoutesFromAPI; + ExtractRoutesFromAPI & + ExtractRoutesFromAPI; declare module '@rocket.chat/rest-typings' { // eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/no-empty-interface - interface Endpoints extends RoomEndpoints {} + interface Endpoints extends RoomEndpoints { } } diff --git a/packages/rest-typings/src/v1/rooms.ts b/packages/rest-typings/src/v1/rooms.ts index 273cc76869d7f..57cc0f38286e7 100644 --- a/packages/rest-typings/src/v1/rooms.ts +++ b/packages/rest-typings/src/v1/rooms.ts @@ -854,9 +854,6 @@ export type RoomsEndpoints = { }; }; - '/v1/rooms.cleanHistory': { - POST: (params: RoomsCleanHistoryProps) => { _id: IRoom['_id']; count: number; success: boolean }; - }; '/v1/rooms.createDiscussion': { POST: (params: RoomsCreateDiscussionProps) => { From ec85cd3912b9daa3e4779a664722653c976875cf Mon Sep 17 00:00:00 2001 From: harsheetsharma Date: Wed, 1 Apr 2026 09:45:12 +0000 Subject: [PATCH 2/2] fix: enforce roomId or roomName requirement in rooms.cleanHistory schema --- apps/meteor/app/api/server/v1/rooms.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/apps/meteor/app/api/server/v1/rooms.ts b/apps/meteor/app/api/server/v1/rooms.ts index 4d38fa09b8b9f..099e75c426330 100644 --- a/apps/meteor/app/api/server/v1/rooms.ts +++ b/apps/meteor/app/api/server/v1/rooms.ts @@ -404,6 +404,10 @@ const roomsCleanHistoryEndpoint = API.v1.post('rooms.cleanHistory', users: { type: 'array', items: { type: 'string' } } }, required: ['latest', 'oldest'], + anyOf: [ + { required: ['roomId'] }, + { required: ['roomName'] } + ], additionalProperties: false }), response: {