-
Notifications
You must be signed in to change notification settings - Fork 13.7k
chore: migrate audit DDP methods to /v1/audit.* REST endpoints (EE) #40736
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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. |
| 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. |
| 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win Forward the omnichannel This branch ignores the Minimal fix const { messages } = await getOmnichannelAuditMessages({
type,
msg,
startDate,
endDate,
users,
- visitor: '',
- agent: '',
+ visitor,
+ agent,
});📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||
| 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)); | ||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||
| 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'; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P3: New REST endpoint typings omit the (Based on your team's feedback about keeping API typings aligned with runtime endpoints.) Prompt for AI agents |
||
| 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<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 { | ||
|
|
@@ -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', | ||
| { | ||
|
|
@@ -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 }); | ||
| }, | ||
| ); | ||
There was a problem hiding this comment.
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.
useEndpointis returning the JSON response as-is here, but the table now treats each row as anIAuditLogviaas unknown as IAuditLog. That only hides the contract mismatch from the DDP→REST migration: fields liketsand the audit filter dates will still be serialized whenAuditLogEntryconsumes them.useAuditMutationalready maps REST messages back into client types; the audit-log path needs the same treatment before storing query data.Suggested direction
Also applies to: 72-72
🤖 Prompt for AI Agents