From 1ec42d12360b075c09e2fe4e68447f51facbe4b8 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Fri, 29 May 2026 10:52:00 -0300 Subject: [PATCH 1/2] chore: migrate audit DDP methods to REST endpoints Added three new REST endpoints (EE-only, license: auditing) covering the audit flows that previously only existed as DDP methods: - GET /v1/audit.auditions (auditGetAuditions) - POST /v1/audit.messages (auditGetMessages) - POST /v1/audit.omnichannel.messages (auditGetOmnichannelMessages) Method bodies extracted into apps/meteor/ee/server/lib/audit/functions.ts and reused by both DDP entrypoints (now thin + deprecation-logged) and the new REST handlers. Each REST endpoint: - requires the same per-action permission (can-audit / can-audit-log). - is rate-limited at 10/60s matching the DDP DDPRateLimiter rules. - writes the same AuditLog entry the DDP path produced. - accepts dates as ISO strings on the wire (parsed server-side). Client AuditLogTable + useAuditMutation swapped from useMethod to useEndpoint; date params serialized via toISOString(). DDP methods stay registered with deprecation logs pointing at the new routes until 9.0.0. Co-Authored-By: Claude Opus 4.7 --- .../ddp-migrate-batch6-audit-callers.md | 5 + .changeset/rest-audit-endpoints.md | 11 + .../views/audit/components/AuditLogTable.tsx | 10 +- .../views/audit/hooks/useAuditMutation.ts | 23 +- apps/meteor/ee/server/api/audit.ts | 168 ++++++++++++- apps/meteor/ee/server/lib/audit/functions.ts | 227 ++++++++++++++++++ apps/meteor/ee/server/lib/audit/methods.ts | 202 ++-------------- 7 files changed, 445 insertions(+), 201 deletions(-) create mode 100644 .changeset/ddp-migrate-batch6-audit-callers.md create mode 100644 .changeset/rest-audit-endpoints.md create mode 100644 apps/meteor/ee/server/lib/audit/functions.ts diff --git a/.changeset/ddp-migrate-batch6-audit-callers.md b/.changeset/ddp-migrate-batch6-audit-callers.md new file mode 100644 index 0000000000000..3b07e86cf51ec --- /dev/null +++ b/.changeset/ddp-migrate-batch6-audit-callers.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Migrated the audit panel (`AuditLogTable`, `useAuditMutation`) from the three `auditGet*` DDP methods to the new `/v1/audit.*` REST endpoints. DDP methods stay registered with deprecation logs pointing at the new routes until 9.0.0. diff --git a/.changeset/rest-audit-endpoints.md b/.changeset/rest-audit-endpoints.md new file mode 100644 index 0000000000000..59f52db3453fd --- /dev/null +++ b/.changeset/rest-audit-endpoints.md @@ -0,0 +1,11 @@ +--- +'@rocket.chat/meteor': minor +--- + +Added three new REST endpoints under `/v1/audit.*` (EE-only, requires the `auditing` license) covering the audit flows that previously only existed as DDP methods: + +- `GET /v1/audit.auditions?startDate=&endDate=` → `{ auditions: IAuditLog[] }` (replaces `auditGetAuditions`, `can-audit-log`) +- `POST /v1/audit.messages` body `{ rid?, startDate, endDate, users, msg, type, visitor?, agent? }` → `{ messages: IMessage[] }` (replaces `auditGetMessages`, `can-audit`) +- `POST /v1/audit.omnichannel.messages` body `{ startDate, endDate, users, msg, type, visitor?, agent? }` → `{ messages: IMessage[] }` (replaces `auditGetOmnichannelMessages`, `can-audit`) + +Each endpoint is rate-limited at 10 requests / 60s (matching the DDP `DDPRateLimiter` rules) and writes the same `AuditLog` entry the DDP methods produced. Dates are serialized as ISO strings on the wire. The DDP methods remain registered with deprecation logs pointing at the new routes until 9.0.0. diff --git a/apps/meteor/client/views/audit/components/AuditLogTable.tsx b/apps/meteor/client/views/audit/components/AuditLogTable.tsx index 38a49e0b01976..36e5c376c2574 100644 --- a/apps/meteor/client/views/audit/components/AuditLogTable.tsx +++ b/apps/meteor/client/views/audit/components/AuditLogTable.tsx @@ -1,6 +1,6 @@ import { Field, FieldLabel, FieldRow } from '@rocket.chat/fuselage'; import { GenericTable, GenericTableHeaderCell, GenericTableBody, GenericTableLoadingRow, GenericTableHeader } from '@rocket.chat/ui-client'; -import { useTranslation, useMethod } from '@rocket.chat/ui-contexts'; +import { useTranslation, useEndpoint } from '@rocket.chat/ui-contexts'; import { useQuery } from '@tanstack/react-query'; import type { ReactElement } from 'react'; import { useState } from 'react'; @@ -19,14 +19,18 @@ const AuditLogTable = (): ReactElement => { end: createEndOfToday(), })); - const getAudits = useMethod('auditGetAuditions'); + const getAudits = useEndpoint('GET', '/v1/audit.auditions'); const { data, isLoading, isSuccess } = useQuery({ queryKey: ['audits', dateRange], queryFn: async () => { const { start, end } = dateRange; - return getAudits({ startDate: start ?? new Date(0), endDate: end ?? new Date() }); + const { auditions } = await getAudits({ + startDate: (start ?? new Date(0)).toISOString(), + endDate: (end ?? new Date()).toISOString(), + }); + return auditions; }, meta: { apiErrorToastMessage: true, diff --git a/apps/meteor/client/views/audit/hooks/useAuditMutation.ts b/apps/meteor/client/views/audit/hooks/useAuditMutation.ts index 68f7a27abf02b..40574ffa31a93 100644 --- a/apps/meteor/client/views/audit/hooks/useAuditMutation.ts +++ b/apps/meteor/client/views/audit/hooks/useAuditMutation.ts @@ -1,39 +1,44 @@ import type { IAuditLog } from '@rocket.chat/core-typings'; -import { useMethod } from '@rocket.chat/ui-contexts'; +import { useEndpoint } from '@rocket.chat/ui-contexts'; import { useMutation } from '@tanstack/react-query'; import type { AuditFields } from './useAuditForm'; export const useAuditMutation = (type: IAuditLog['fields']['type']) => { - const getAuditMessages = useMethod('auditGetMessages'); - const getOmnichannelAuditMessages = useMethod('auditGetOmnichannelMessages'); + const getAuditMessages = useEndpoint('POST', '/v1/audit.messages'); + const getOmnichannelAuditMessages = useEndpoint('POST', '/v1/audit.omnichannel.messages'); return useMutation({ mutationKey: ['audit'] as const, mutationFn: async ({ msg, dateRange, rid, users, visitor, agent }: AuditFields) => { + const startDate = (dateRange.start ?? new Date(0)).toISOString(); + const endDate = (dateRange.end ?? new Date()).toISOString(); + if (type === 'l') { - return getOmnichannelAuditMessages({ + const { messages } = await getOmnichannelAuditMessages({ type, msg, - startDate: dateRange.start ?? new Date(0), - endDate: dateRange.end ?? new Date(), + startDate, + endDate, users, visitor: '', agent: '', }); + return messages; } - return getAuditMessages({ + const { messages } = await getAuditMessages({ type, msg, - startDate: dateRange.start ?? new Date(0), - endDate: dateRange.end ?? new Date(), + startDate, + endDate, rid, users, visitor, agent, }); + return messages; }, }); }; diff --git a/apps/meteor/ee/server/api/audit.ts b/apps/meteor/ee/server/api/audit.ts index 77d4ebf10cf7c..2df48fe12f78f 100644 --- a/apps/meteor/ee/server/api/audit.ts +++ b/apps/meteor/ee/server/api/audit.ts @@ -1,4 +1,4 @@ -import type { IUser, IRoom } from '@rocket.chat/core-typings'; +import type { IAuditLog, IMessage, IUser, IRoom } from '@rocket.chat/core-typings'; import { Rooms, AuditLog, ServerEvents } from '@rocket.chat/models'; import { isServerEventsAuditSettingsProps, ajv, ajvQuery } from '@rocket.chat/rest-typings'; import type { PaginatedRequest, PaginatedResult } from '@rocket.chat/rest-typings'; @@ -7,6 +7,7 @@ import { convertSubObjectsIntoPaths } from '@rocket.chat/tools'; import { API } from '../../../app/api/server/api'; import { getPaginationItems } from '../../../app/api/server/helpers/getPaginationItems'; import { findUsersOfRoom } from '../../../server/lib/findUsersOfRoom'; +import { auditGetAuditionsMethod, auditGetMessagesMethod, auditGetOmnichannelMessagesMethod } from '../lib/audit/functions'; type AuditRoomMembersParams = PaginatedRequest<{ roomId: string; @@ -28,6 +29,76 @@ const auditRoomMembersSchema = { export const isAuditRoomMembersProps = ajvQuery.compile(auditRoomMembersSchema); +type AuditAuditionsParams = { startDate: string; endDate: string }; + +const auditAuditionsSchema = { + type: 'object', + properties: { + startDate: { type: 'string', minLength: 1 }, + endDate: { type: 'string', minLength: 1 }, + }, + required: ['startDate', 'endDate'], + additionalProperties: false, +}; + +const isAuditAuditionsProps = ajvQuery.compile(auditAuditionsSchema); + +type AuditMessagesPayload = { + rid?: string; + startDate: string; + endDate: string; + users: string[]; + msg: string; + type: string; + visitor?: string; + agent?: string; +}; + +const auditMessagesSchema = { + type: 'object', + properties: { + rid: { type: 'string' }, + startDate: { type: 'string', minLength: 1 }, + endDate: { type: 'string', minLength: 1 }, + users: { type: 'array', items: { type: 'string' } }, + msg: { type: 'string' }, + type: { type: 'string' }, + visitor: { type: 'string' }, + agent: { type: 'string' }, + }, + required: ['startDate', 'endDate', 'users', 'msg', 'type'], + additionalProperties: false, +}; + +const isAuditMessagesProps = ajv.compile(auditMessagesSchema); + +type AuditOmnichannelMessagesPayload = { + startDate: string; + endDate: string; + users: string[]; + msg: string; + type: string; + visitor?: string; + agent?: string; +}; + +const auditOmnichannelMessagesSchema = { + type: 'object', + properties: { + startDate: { type: 'string', minLength: 1 }, + endDate: { type: 'string', minLength: 1 }, + users: { type: 'array', items: { type: 'string' } }, + msg: { type: 'string' }, + type: { type: 'string' }, + visitor: { type: 'string' }, + agent: { type: 'string' }, + }, + required: ['startDate', 'endDate', 'users', 'msg', 'type'], + additionalProperties: false, +}; + +const isAuditOmnichannelMessagesProps = ajv.compile(auditOmnichannelMessagesSchema); + declare module '@rocket.chat/rest-typings' { // eslint-disable-next-line @typescript-eslint/naming-convention interface Endpoints { @@ -36,9 +107,29 @@ declare module '@rocket.chat/rest-typings' { params: AuditRoomMembersParams, ) => PaginatedResult<{ members: Pick[] }>; }; + + '/v1/audit.auditions': { + GET: (params: AuditAuditionsParams) => { auditions: IAuditLog[] }; + }; + + '/v1/audit.messages': { + POST: (params: AuditMessagesPayload) => { messages: IMessage[] }; + }; + + '/v1/audit.omnichannel.messages': { + POST: (params: AuditOmnichannelMessagesPayload) => { messages: IMessage[] }; + }; } } +const parseDateOrFail = (value: string, name: string): Date => { + const ts = Date.parse(value); + if (Number.isNaN(ts)) { + throw new Error(`The "${name}" parameter must be a valid date.`); + } + return new Date(ts); +}; + API.v1.addRoute( 'audit/rooms.members', { @@ -194,3 +285,78 @@ API.v1.get( }); }, ); + +API.v1.get( + 'audit.auditions', + { + authRequired: true, + permissionsRequired: ['can-audit-log'], + query: isAuditAuditionsProps, + license: ['auditing'], + rateLimiterOptions: { numRequestsAllowed: 10, intervalTimeInMS: 60000 }, + }, + async function action() { + const startDate = parseDateOrFail(this.queryParams.startDate, 'startDate'); + const endDate = parseDateOrFail(this.queryParams.endDate, 'endDate'); + + const auditions = await auditGetAuditionsMethod(this.userId, startDate, endDate); + return API.v1.success({ auditions }); + }, +); + +API.v1.post( + 'audit.messages', + { + authRequired: true, + permissionsRequired: ['can-audit'], + body: isAuditMessagesProps, + license: ['auditing'], + rateLimiterOptions: { numRequestsAllowed: 10, intervalTimeInMS: 60000 }, + }, + async function action() { + const { rid, users, msg, type, visitor, agent } = this.bodyParams; + const startDate = parseDateOrFail(this.bodyParams.startDate, 'startDate'); + const endDate = parseDateOrFail(this.bodyParams.endDate, 'endDate'); + + const messages = await auditGetMessagesMethod(this.userId, { + rid, + startDate, + endDate, + users, + msg, + type, + visitor, + agent, + }); + + return API.v1.success({ messages }); + }, +); + +API.v1.post( + 'audit.omnichannel.messages', + { + authRequired: true, + permissionsRequired: ['can-audit'], + body: isAuditOmnichannelMessagesProps, + license: ['auditing'], + rateLimiterOptions: { numRequestsAllowed: 10, intervalTimeInMS: 60000 }, + }, + async function action() { + const { users, msg, type, visitor, agent } = this.bodyParams; + const startDate = parseDateOrFail(this.bodyParams.startDate, 'startDate'); + const endDate = parseDateOrFail(this.bodyParams.endDate, 'endDate'); + + const messages = await auditGetOmnichannelMessagesMethod(this.userId, { + startDate, + endDate, + users, + msg, + type, + visitor, + agent, + }); + + return API.v1.success({ messages }); + }, +); diff --git a/apps/meteor/ee/server/lib/audit/functions.ts b/apps/meteor/ee/server/lib/audit/functions.ts new file mode 100644 index 0000000000000..8b3457ee16fc1 --- /dev/null +++ b/apps/meteor/ee/server/lib/audit/functions.ts @@ -0,0 +1,227 @@ +import type { ILivechatAgent, ILivechatVisitor, IMessage, IRoom, IUser, IAuditLog } from '@rocket.chat/core-typings'; +import { LivechatRooms, Messages, Rooms, Users, AuditLog } from '@rocket.chat/models'; +import { escapeRegExp } from '@rocket.chat/string-helpers'; +import { isTruthy } from '@rocket.chat/tools'; +import { Meteor } from 'meteor/meteor'; +import type { Filter } from 'mongodb'; + +import { hasPermissionAsync } from '../../../../app/authorization/server/functions/hasPermission'; +import { updateCounter } from '../../../../app/statistics/server'; +import { callbacks } from '../../../../server/lib/callbacks'; +import { i18n } from '../../../../server/lib/i18n'; + +const getValue = (room: IRoom | null) => room && { rids: [room._id], name: room.name }; + +const getUsersIdFromUserName = async (usernames: IUser['username'][]) => { + const users = usernames ? await Users.findByUsernames(usernames.filter(isTruthy)).toArray() : undefined; + + return users?.filter(isTruthy).map((userId) => userId._id); +}; + +const getRoomInfoByAuditParams = async ({ + type, + roomId: rid, + users: usernames, + visitor, + agent, + userId, +}: { + type: string; + roomId: IRoom['_id']; + users: NonNullable[]; + visitor: ILivechatVisitor['_id']; + agent: ILivechatAgent['_id']; + userId: string; +}) => { + if (rid) { + return getValue(await Rooms.findOne({ _id: rid, abacAttributes: { $exists: false } })); + } + + if (type === 'd') { + return getValue(await Rooms.findDirectRoomContainingAllUsernames(usernames)); + } + + if (type === 'l') { + console.warn('Deprecation Warning! This method will be removed in the next version (4.0.0)'); + const extraQuery = await callbacks.run('livechat.applyRoomRestrictions', {}, { userId }); + const rooms: IRoom[] = await LivechatRooms.findByVisitorIdAndAgentId( + visitor, + agent, + { + projection: { _id: 1 }, + }, + extraQuery, + ).toArray(); + return rooms?.length ? { rids: rooms.map(({ _id }) => _id), name: i18n.t('Omnichannel') } : undefined; + } +}; + +const requireAuditor = async (userId: string | null): Promise => { + if (!userId) { + throw new Meteor.Error('Not allowed'); + } + + const user = await Users.findOneById(userId); + if (!user || !(await hasPermissionAsync(user._id, 'can-audit'))) { + throw new Meteor.Error('Not allowed'); + } + return user; +}; + +type AuditMessagesParams = { + rid?: IRoom['_id']; + startDate: Date; + endDate: Date; + users: NonNullable[]; + msg: IMessage['msg']; + type: string; + visitor?: ILivechatVisitor['_id']; + agent?: ILivechatAgent['_id']; +}; + +export const auditGetMessagesMethod = async (userId: string | null, params: AuditMessagesParams): Promise => { + const { rid, startDate, endDate, users: usernames, msg, type, visitor, agent } = params; + + const user = await requireAuditor(userId); + + const userFields = { + _id: user._id, + username: user.username, + ...(user.name && { name: user.name }), + ...(user.avatarETag && { avatarETag: user.avatarETag }), + }; + + let rids; + let name; + + const query: Filter = { + ts: { + $gt: startDate, + $lt: endDate, + }, + }; + + if (type === 'u') { + const usersId = await getUsersIdFromUserName(usernames); + query['u._id'] = { $in: usersId }; + + const abacRooms = await Rooms.findAllPrivateRoomsWithAbacAttributes({ projection: { _id: 1 } }) + .map((doc) => doc._id) + .toArray(); + + query.rid = { $nin: abacRooms }; + } else { + const roomInfo = await getRoomInfoByAuditParams({ + type, + roomId: rid ?? '', + users: usernames, + visitor: visitor ?? '', + agent: agent ?? '', + userId: user._id, + }); + if (!roomInfo) { + throw new Meteor.Error(`Room doesn't exist`); + } + + rids = roomInfo.rids; + name = roomInfo.name; + query.rid = { $in: rids }; + } + + if (msg) { + const regex = new RegExp(escapeRegExp(msg).trim(), 'i'); + query.msg = regex; + } + + const messages = await Messages.find(query).toArray(); + + await AuditLog.insertOne({ + ts: new Date(), + results: messages.length, + u: userFields, + fields: { msg, users: usernames, rids, room: name, startDate, endDate, type, visitor, agent }, + }); + + updateCounter({ settingsId: 'Message_Auditing_Panel_Load_Count' }); + + return messages; +}; + +type AuditOmnichannelMessagesParams = { + startDate: Date; + endDate: Date; + users: NonNullable[]; + msg: IMessage['msg']; + type: string; + visitor?: ILivechatVisitor['_id']; + agent?: ILivechatAgent['_id']; +}; + +export const auditGetOmnichannelMessagesMethod = async ( + userId: string | null, + params: AuditOmnichannelMessagesParams, +): Promise => { + const { startDate, endDate, users: usernames, msg, type, visitor, agent } = params; + + const user = await requireAuditor(userId); + + const userFields = { + _id: user._id, + username: user.username, + ...(user.name && { name: user.name }), + ...(user.avatarETag && { avatarETag: user.avatarETag }), + }; + + const rooms: IRoom[] = await LivechatRooms.findByVisitorIdAndAgentId(visitor, agent, { + projection: { _id: 1 }, + }).toArray(); + const rids = rooms?.length ? rooms.map(({ _id }) => _id) : undefined; + const name = i18n.t('Omnichannel'); + + const query: Filter = { + rid: { $in: rids }, + ts: { + $gt: startDate, + $lt: endDate, + }, + }; + + if (msg) { + const regex = new RegExp(escapeRegExp(msg).trim(), 'i'); + query.msg = regex; + } + + const messages = await Messages.find(query).toArray(); + + await AuditLog.insertOne({ + ts: new Date(), + results: messages.length, + u: userFields, + fields: { msg, users: usernames, rids, room: name, startDate, endDate, type, visitor, agent }, + }); + + return messages; +}; + +export const auditGetAuditionsMethod = async (userId: string | null, startDate: Date, endDate: Date): Promise => { + if (!userId || !(await hasPermissionAsync(userId, 'can-audit-log'))) { + throw new Meteor.Error('Not allowed'); + } + return AuditLog.find( + { + ts: { + $gt: startDate, + $lt: endDate, + }, + }, + { + projection: { + 'u.services': 0, + 'u.roles': 0, + 'u.lastLogin': 0, + 'u.statusConnection': 0, + 'u.emails': 0, + }, + }, + ).toArray(); +}; diff --git a/apps/meteor/ee/server/lib/audit/methods.ts b/apps/meteor/ee/server/lib/audit/methods.ts index 592e45e9437d0..2c15060485313 100644 --- a/apps/meteor/ee/server/lib/audit/methods.ts +++ b/apps/meteor/ee/server/lib/audit/methods.ts @@ -1,64 +1,11 @@ import type { ILivechatAgent, ILivechatVisitor, IMessage, IRoom, IUser, IAuditLog } from '@rocket.chat/core-typings'; import type { ServerMethods } from '@rocket.chat/ddp-client'; -import { LivechatRooms, Messages, Rooms, Users, AuditLog } from '@rocket.chat/models'; -import { escapeRegExp } from '@rocket.chat/string-helpers'; -import { isTruthy } from '@rocket.chat/tools'; import { check } from 'meteor/check'; import { DDPRateLimiter } from 'meteor/ddp-rate-limiter'; import { Meteor } from 'meteor/meteor'; -import type { Filter } from 'mongodb'; -import { hasPermissionAsync } from '../../../../app/authorization/server/functions/hasPermission'; -import { updateCounter } from '../../../../app/statistics/server'; -import { callbacks } from '../../../../server/lib/callbacks'; -import { i18n } from '../../../../server/lib/i18n'; - -const getValue = (room: IRoom | null) => room && { rids: [room._id], name: room.name }; - -const getUsersIdFromUserName = async (usernames: IUser['username'][]) => { - const users = usernames ? await Users.findByUsernames(usernames.filter(isTruthy)).toArray() : undefined; - - return users?.filter(isTruthy).map((userId) => userId._id); -}; - -const getRoomInfoByAuditParams = async ({ - type, - roomId: rid, - users: usernames, - visitor, - agent, - userId, -}: { - type: string; - roomId: IRoom['_id']; - users: NonNullable[]; - visitor: ILivechatVisitor['_id']; - agent: ILivechatAgent['_id']; - userId: string; -}) => { - if (rid) { - // When ABAC is enabled, only rooms without ABAC attributes are considered for auditing by room ID. - return getValue(await Rooms.findOne({ _id: rid, abacAttributes: { $exists: false } })); - } - - if (type === 'd') { - return getValue(await Rooms.findDirectRoomContainingAllUsernames(usernames)); - } - - if (type === 'l') { - console.warn('Deprecation Warning! This method will be removed in the next version (4.0.0)'); - const extraQuery = await callbacks.run('livechat.applyRoomRestrictions', {}, { userId }); - const rooms: IRoom[] = await LivechatRooms.findByVisitorIdAndAgentId( - visitor, - agent, - { - projection: { _id: 1 }, - }, - extraQuery, - ).toArray(); - return rooms?.length ? { rids: rooms.map(({ _id }) => _id), name: i18n.t('Omnichannel') } : undefined; - } -}; +import { auditGetAuditionsMethod, auditGetMessagesMethod, auditGetOmnichannelMessagesMethod } from './functions'; +import { methodDeprecationLogger } from '../../../../app/lib/server/lib/deprecationWarningLogger'; declare module '@rocket.chat/ddp-client' { // eslint-disable-next-line @typescript-eslint/naming-convention @@ -87,144 +34,23 @@ declare module '@rocket.chat/ddp-client' { } Meteor.methods({ - async auditGetOmnichannelMessages({ startDate, endDate, users: usernames, msg, type, visitor, agent }) { - check(startDate, Date); - check(endDate, Date); - - const user = (await Meteor.userAsync()) as IUser; - if (!user || !(await hasPermissionAsync(user._id, 'can-audit'))) { - throw new Meteor.Error('Not allowed'); - } - - const userFields = { - _id: user._id, - username: user.username, - ...(user.name && { name: user.name }), - ...(user.avatarETag && { avatarETag: user.avatarETag }), - }; - - const rooms: IRoom[] = await LivechatRooms.findByVisitorIdAndAgentId(visitor, agent, { - projection: { _id: 1 }, - }).toArray(); - const rids = rooms?.length ? rooms.map(({ _id }) => _id) : undefined; - const name = i18n.t('Omnichannel'); - - const query: Filter = { - rid: { $in: rids }, - ts: { - $gt: startDate, - $lt: endDate, - }, - }; - - if (msg) { - const regex = new RegExp(escapeRegExp(msg).trim(), 'i'); - query.msg = regex; - } - const messages = await Messages.find(query).toArray(); - - // Once the filter is applied, messages will be shown and a log containing all filters will be saved for further auditing. - - await AuditLog.insertOne({ - ts: new Date(), - results: messages.length, - u: userFields, - fields: { msg, users: usernames, rids, room: name, startDate, endDate, type, visitor, agent }, - }); - - return messages; + async auditGetOmnichannelMessages(params) { + methodDeprecationLogger.method('auditGetOmnichannelMessages', '9.0.0', '/v1/audit.omnichannel.messages'); + check(params.startDate, Date); + check(params.endDate, Date); + return auditGetOmnichannelMessagesMethod(Meteor.userId(), params); }, - async auditGetMessages({ rid, startDate, endDate, users: usernames, msg, type, visitor, agent }) { - check(startDate, Date); - check(endDate, Date); - - const user = (await Meteor.userAsync()) as IUser; - if (!user || !(await hasPermissionAsync(user._id, 'can-audit'))) { - throw new Meteor.Error('Not allowed'); - } - - const userFields = { - _id: user._id, - username: user.username, - ...(user.name && { name: user.name }), - ...(user.avatarETag && { avatarETag: user.avatarETag }), - }; - - let rids; - let name; - - const query: Filter = { - ts: { - $gt: startDate, - $lt: endDate, - }, - }; - - if (type === 'u') { - const usersId = await getUsersIdFromUserName(usernames); - query['u._id'] = { $in: usersId }; - - const abacRooms = await Rooms.findAllPrivateRoomsWithAbacAttributes({ projection: { _id: 1 } }) - .map((doc) => doc._id) - .toArray(); - - query.rid = { $nin: abacRooms }; - } else { - const roomInfo = await getRoomInfoByAuditParams({ type, roomId: rid, users: usernames, visitor, agent, userId: user._id }); - if (!roomInfo) { - throw new Meteor.Error(`Room doesn't exist`); - } - - rids = roomInfo.rids; - name = roomInfo.name; - query.rid = { $in: rids }; - } - - if (msg) { - const regex = new RegExp(escapeRegExp(msg).trim(), 'i'); - query.msg = regex; - } - - const messages = await Messages.find(query).toArray(); - - // Once the filter is applied, messages will be shown and a log containing all filters will be saved for further auditing. - - await AuditLog.insertOne({ - ts: new Date(), - results: messages.length, - u: userFields, - fields: { msg, users: usernames, rids, room: name, startDate, endDate, type, visitor, agent }, - }); - - updateCounter({ settingsId: 'Message_Auditing_Panel_Load_Count' }); - - return messages; + async auditGetMessages(params) { + methodDeprecationLogger.method('auditGetMessages', '9.0.0', '/v1/audit.messages'); + check(params.startDate, Date); + check(params.endDate, Date); + return auditGetMessagesMethod(Meteor.userId(), params); }, async auditGetAuditions({ startDate, endDate }) { + methodDeprecationLogger.method('auditGetAuditions', '9.0.0', '/v1/audit.auditions'); check(startDate, Date); check(endDate, Date); - const uid = Meteor.userId(); - if (!uid || !(await hasPermissionAsync(uid, 'can-audit-log'))) { - throw new Meteor.Error('Not allowed'); - } - return AuditLog.find( - { - // 'u._id': userId, - ts: { - $gt: startDate, - $lt: endDate, - }, - }, - { - projection: { - 'u.services': 0, - 'u.roles': 0, - 'u.lastLogin': 0, - 'u.statusConnection': 0, - 'u.emails': 0, - }, - }, - ).toArray(); + return auditGetAuditionsMethod(Meteor.userId(), startDate, endDate); }, }); From 4a5e8a27e57c4697e5577832bda4cc51aa156d7b Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Fri, 29 May 2026 11:09:37 -0300 Subject: [PATCH 2/2] fix: address TS errors in audit batch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add response schemas to /v1/audit.{auditions,messages,omnichannel.messages} so TypedOptions infers queryParams/bodyParams correctly. - mapMessageFromApi() each REST message in useAuditMutation so caller AuditResult receives IMessage[] (Date) instead of Serialized[]. - Cast AuditLogEntry value through unknown→IAuditLog since the REST response carries dates as strings while the consumer reads IAuditLog (the component formats the Date itself, so the cast is just to satisfy TS — same pattern other audit-adjacent code uses). Co-Authored-By: Claude Opus 4.7 --- .../views/audit/components/AuditLogTable.tsx | 3 +- .../views/audit/hooks/useAuditMutation.ts | 9 ++-- apps/meteor/ee/server/api/audit.ts | 42 +++++++++++++++++++ 3 files changed, 49 insertions(+), 5 deletions(-) diff --git a/apps/meteor/client/views/audit/components/AuditLogTable.tsx b/apps/meteor/client/views/audit/components/AuditLogTable.tsx index 36e5c376c2574..fbe188839b052 100644 --- a/apps/meteor/client/views/audit/components/AuditLogTable.tsx +++ b/apps/meteor/client/views/audit/components/AuditLogTable.tsx @@ -1,3 +1,4 @@ +import type { IAuditLog } from '@rocket.chat/core-typings'; import { Field, FieldLabel, FieldRow } from '@rocket.chat/fuselage'; import { GenericTable, GenericTableHeaderCell, GenericTableBody, GenericTableLoadingRow, GenericTableHeader } from '@rocket.chat/ui-client'; import { useTranslation, useEndpoint } from '@rocket.chat/ui-contexts'; @@ -69,7 +70,7 @@ const AuditLogTable = (): ReactElement => { {headers} {data.map((auditLog) => ( - + ))} diff --git a/apps/meteor/client/views/audit/hooks/useAuditMutation.ts b/apps/meteor/client/views/audit/hooks/useAuditMutation.ts index 40574ffa31a93..5a6d001bbd987 100644 --- a/apps/meteor/client/views/audit/hooks/useAuditMutation.ts +++ b/apps/meteor/client/views/audit/hooks/useAuditMutation.ts @@ -1,8 +1,9 @@ -import type { IAuditLog } from '@rocket.chat/core-typings'; +import type { IAuditLog, IMessage } from '@rocket.chat/core-typings'; import { useEndpoint } from '@rocket.chat/ui-contexts'; import { useMutation } from '@tanstack/react-query'; import type { AuditFields } from './useAuditForm'; +import { mapMessageFromApi } from '../../../lib/utils/mapMessageFromApi'; export const useAuditMutation = (type: IAuditLog['fields']['type']) => { const getAuditMessages = useEndpoint('POST', '/v1/audit.messages'); @@ -11,7 +12,7 @@ export const useAuditMutation = (type: IAuditLog['fields']['type']) => { return useMutation({ mutationKey: ['audit'] as const, - mutationFn: async ({ msg, dateRange, rid, users, visitor, agent }: AuditFields) => { + mutationFn: async ({ msg, dateRange, rid, users, visitor, agent }: AuditFields): Promise => { const startDate = (dateRange.start ?? new Date(0)).toISOString(); const endDate = (dateRange.end ?? new Date()).toISOString(); @@ -25,7 +26,7 @@ export const useAuditMutation = (type: IAuditLog['fields']['type']) => { visitor: '', agent: '', }); - return messages; + return messages.map((message) => mapMessageFromApi(message)); } const { messages } = await getAuditMessages({ @@ -38,7 +39,7 @@ export const useAuditMutation = (type: IAuditLog['fields']['type']) => { visitor, agent, }); - return messages; + return messages.map((message) => mapMessageFromApi(message)); }, }); }; diff --git a/apps/meteor/ee/server/api/audit.ts b/apps/meteor/ee/server/api/audit.ts index 2df48fe12f78f..a233dabfb278b 100644 --- a/apps/meteor/ee/server/api/audit.ts +++ b/apps/meteor/ee/server/api/audit.ts @@ -286,6 +286,36 @@ API.v1.get( }, ); +const auditAuditionsResponseSchema = ajv.compile<{ auditions: IAuditLog[] }>({ + type: 'object', + properties: { + auditions: { type: 'array', items: { type: 'object' } }, + success: { type: 'boolean', enum: [true] }, + }, + required: ['auditions', 'success'], + additionalProperties: false, +}); + +const auditMessagesResponseSchema = ajv.compile<{ messages: IMessage[] }>({ + type: 'object', + properties: { + messages: { type: 'array', items: { type: 'object' } }, + success: { type: 'boolean', enum: [true] }, + }, + required: ['messages', 'success'], + additionalProperties: false, +}); + +const auditErrorResponseSchema = ajv.compile({ + type: 'object', + properties: { + success: { type: 'boolean', enum: [false] }, + error: { type: 'string' }, + errorType: { type: 'string' }, + }, + required: ['success', 'error'], +}); + API.v1.get( 'audit.auditions', { @@ -294,6 +324,10 @@ API.v1.get( query: isAuditAuditionsProps, license: ['auditing'], rateLimiterOptions: { numRequestsAllowed: 10, intervalTimeInMS: 60000 }, + response: { + 200: auditAuditionsResponseSchema, + 400: auditErrorResponseSchema, + }, }, async function action() { const startDate = parseDateOrFail(this.queryParams.startDate, 'startDate'); @@ -312,6 +346,10 @@ API.v1.post( body: isAuditMessagesProps, license: ['auditing'], rateLimiterOptions: { numRequestsAllowed: 10, intervalTimeInMS: 60000 }, + response: { + 200: auditMessagesResponseSchema, + 400: auditErrorResponseSchema, + }, }, async function action() { const { rid, users, msg, type, visitor, agent } = this.bodyParams; @@ -341,6 +379,10 @@ API.v1.post( body: isAuditOmnichannelMessagesProps, license: ['auditing'], rateLimiterOptions: { numRequestsAllowed: 10, intervalTimeInMS: 60000 }, + response: { + 200: auditMessagesResponseSchema, + 400: auditErrorResponseSchema, + }, }, async function action() { const { users, msg, type, visitor, agent } = this.bodyParams;