Skip to content
Draft
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 type { ReactElement } from 'react';
import { useState } from 'react';
Expand All @@ -19,14 +20,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,
Expand Down Expand Up @@ -65,7 +70,7 @@ const AuditLogTable = (): ReactElement => {
<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: '',
});
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';
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