Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/ddp-migrate-batch6-audit-callers.md
Original file line number Diff line number Diff line change
@@ -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.
11 changes: 11 additions & 0 deletions .changeset/rest-audit-endpoints.md
Original file line number Diff line number Diff line change
@@ -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.
13 changes: 9 additions & 4 deletions apps/meteor/client/views/audit/components/AuditLogTable.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
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, useMethod } from '@rocket.chat/ui-contexts';
import { useTranslation, useEndpoint } from '@rocket.chat/ui-contexts';
import { useQuery } from '@tanstack/react-query';
import { useState } from 'react';

Expand All @@ -18,14 +19,18 @@ const AuditLogTable = () => {
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;
Comment on lines 27 to +33

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Deserialize the REST payload instead of force-casting it.

useEndpoint is returning the JSON response as-is here, but the table now treats each row as an IAuditLog via as unknown as IAuditLog. That only hides the contract mismatch from the DDP→REST migration: fields like ts and the audit filter dates will still be serialized when AuditLogEntry consumes them. useAuditMutation already maps REST messages back into client types; the audit-log path needs the same treatment before storing query data.

Suggested direction
+import { mapAuditLogFromApi } from '../../../lib/utils/mapAuditLogFromApi';
...
 			const { auditions } = await getAudits({
 				startDate: (start ?? new Date(0)).toISOString(),
 				endDate: (end ?? new Date()).toISOString(),
 			});
-			return auditions;
+			return auditions.map(mapAuditLogFromApi);
...
-							<AuditLogEntry key={auditLog._id} value={auditLog as unknown as IAuditLog} />
+							<AuditLogEntry key={auditLog._id} value={auditLog} />

Also applies to: 72-72

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/meteor/client/views/audit/components/AuditLogTable.tsx` around lines 27
- 33, The audit log query is still force-casting the REST response into
IAuditLog instead of converting it to the client shape. Update AuditLogTable’s
useEndpoint/queryFn path to deserialize the payload before caching it, mapping
REST fields like ts and date values into the IAuditLog structure the table and
AuditLogEntry expect, similar to how useAuditMutation handles REST-to-client
conversion.

},
meta: {
apiErrorToastMessage: true,
Expand Down Expand Up @@ -64,7 +69,7 @@ const AuditLogTable = () => {
<GenericTableHeader>{headers}</GenericTableHeader>
<GenericTableBody>
{data.map((auditLog) => (
<AuditLogEntry key={auditLog._id} value={auditLog} />
<AuditLogEntry key={auditLog._id} value={auditLog as unknown as IAuditLog} />
))}
</GenericTableBody>
</GenericTable>
Expand Down
28 changes: 17 additions & 11 deletions apps/meteor/client/views/audit/hooks/useAuditMutation.ts
Original file line number Diff line number Diff line change
@@ -1,39 +1,45 @@
import type { IAuditLog } from '@rocket.chat/core-typings';
import { useMethod } from '@rocket.chat/ui-contexts';
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 = 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) => {
mutationFn: async ({ msg, dateRange, rid, users, visitor, agent }: AuditFields): Promise<IMessage[]> => {
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: '',
Comment on lines 19 to 27

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Forward the omnichannel visitor and agent filters.

This branch ignores the AuditFields values and always posts empty strings, so any omnichannel audit filtered by visitor or agent will return broader results than the user requested.

Minimal fix
 				const { messages } = await getOmnichannelAuditMessages({
 					type,
 					msg,
 					startDate,
 					endDate,
 					users,
-					visitor: '',
-					agent: '',
+					visitor,
+					agent,
 				});
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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: '',
if (type === 'l') {
const { messages } = await getOmnichannelAuditMessages({
type,
msg,
startDate,
endDate,
users,
visitor,
agent,
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/meteor/client/views/audit/hooks/useAuditMutation.ts` around lines 19 -
27, The `useAuditMutation` branch for `type === 'l'` is ignoring the
`AuditFields` filter values by hardcoding `visitor` and `agent` to empty
strings. Update the `getOmnichannelAuditMessages` call to forward the actual
`visitor` and `agent` values from the current audit filters, using the existing
`AuditFields` data in `useAuditMutation` so omnichannel searches stay scoped
correctly.

});
return messages.map((message) => mapMessageFromApi(message));
}

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.map((message) => mapMessageFromApi(message));
},
});
};
210 changes: 209 additions & 1 deletion apps/meteor/ee/server/api/audit.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { IUser, IRoom } from '@rocket.chat/core-typings';
import type { IAuditLog, IMessage, IUser, IRoom } from '@rocket.chat/core-typings';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: New REST endpoint typings omit the success field returned at runtime, so API consumers get a misleading contract for these migrated endpoints.

(Based on your team's feedback about keeping API typings aligned with runtime endpoints.)

View Feedback

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/meteor/ee/server/api/audit.ts, line 112:

<comment>New REST endpoint typings omit the `success` field returned at runtime, so API consumers get a misleading contract for these migrated endpoints.

(Based on your team's feedback about keeping API typings aligned with runtime endpoints.) </comment>

<file context>
@@ -36,9 +107,29 @@ declare module '@rocket.chat/rest-typings' {
 		};
+
+		'/v1/audit.auditions': {
+			GET: (params: AuditAuditionsParams) => { auditions: IAuditLog[] };
+		};
+
</file context>

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';
Expand All @@ -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;
Expand All @@ -28,6 +29,76 @@ const auditRoomMembersSchema = {

export const isAuditRoomMembersProps = ajvQuery.compile<AuditRoomMembersParams>(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<AuditAuditionsParams>(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<AuditMessagesPayload>(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<AuditOmnichannelMessagesPayload>(auditOmnichannelMessagesSchema);

declare module '@rocket.chat/rest-typings' {
// eslint-disable-next-line @typescript-eslint/naming-convention
interface Endpoints {
Expand All @@ -36,9 +107,29 @@ declare module '@rocket.chat/rest-typings' {
params: AuditRoomMembersParams,
) => PaginatedResult<{ members: Pick<IUser, '_id' | 'name' | 'username' | 'status' | '_updatedAt'>[] }>;
};

'/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',
{
Expand Down Expand Up @@ -194,3 +285,120 @@ 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',
{
authRequired: true,
permissionsRequired: ['can-audit-log'],
query: isAuditAuditionsProps,
license: ['auditing'],
rateLimiterOptions: { numRequestsAllowed: 10, intervalTimeInMS: 60000 },
response: {
200: auditAuditionsResponseSchema,
400: auditErrorResponseSchema,
},
},
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 },
response: {
200: auditMessagesResponseSchema,
400: auditErrorResponseSchema,
},
},
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 },
response: {
200: auditMessagesResponseSchema,
400: auditErrorResponseSchema,
},
},
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 });
},
);
Loading
Loading