diff --git a/apps/meteor/ee/server/api/audit.ts b/apps/meteor/ee/server/api/audit.ts index 6fe48fe032d10..18ab668f5185b 100644 --- a/apps/meteor/ee/server/api/audit.ts +++ b/apps/meteor/ee/server/api/audit.ts @@ -1,6 +1,14 @@ import type { 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 { + isServerEventsAuditSettingsProps, + ajv, + ajvQuery, + validateBadRequestErrorResponse, + validateUnauthorizedErrorResponse, + validateForbiddenErrorResponse, + validateNotFoundErrorResponse, +} from '@rocket.chat/rest-typings'; import type { PaginatedRequest, PaginatedResult } from '@rocket.chat/rest-typings'; import { convertSubObjectsIntoPaths } from '@rocket.chat/tools'; @@ -39,61 +47,98 @@ declare module '@rocket.chat/rest-typings' { } } -API.v1.addRoute( +const auditRoomMembersResponseSchema = ajv.compile< + PaginatedResult<{ members: Pick[] }> +>({ + type: 'object', + properties: { + members: { + type: 'array', + items: { + type: 'object', + properties: { + _id: { type: 'string' }, + name: { type: 'string' }, + username: { type: 'string' }, + nickname: { type: 'string' }, + status: { type: 'string', enum: ['online', 'away', 'offline', 'busy', 'disabled'] }, + avatarETag: { type: 'string' }, + federated: { type: 'boolean' }, + _updatedAt: { type: 'string', format: 'date-time' }, + }, + required: ['_id'], + additionalProperties: false, + }, + }, + count: { type: 'number' }, + offset: { type: 'number' }, + total: { type: 'number' }, + success: { type: 'boolean', enum: [true] }, + }, + required: ['members', 'count', 'offset', 'total', 'success'], + additionalProperties: false, +}); + +API.v1.get( 'audit/rooms.members', { authRequired: true, permissionsRequired: ['view-members-list-all-rooms'], - validateParams: isAuditRoomMembersProps, + query: isAuditRoomMembersProps, license: ['auditing'], - }, - { - async get() { - const { roomId, filter } = this.queryParams; - const { count: limit, offset: skip } = await getPaginationItems(this.queryParams); - const { sort } = await this.parseJsonQuery(); - - const room = await Rooms.findOneById>(roomId, { projection: { _id: 1, name: 1, fname: 1 } }); - if (!room) { - return API.v1.notFound(); - } - - const { cursor, totalCount } = findUsersOfRoom({ - rid: room._id, - filter, - skip, - limit, - ...(sort?.username && { sort: { username: sort.username } }), - }); - - const [members, total] = await Promise.all([cursor.toArray(), totalCount]); - - await AuditLog.insertOne({ - ts: new Date(), - results: total, - u: { - _id: this.user._id, - username: this.user.username, - name: this.user.name, - ...(this.user.avatarETag && { avatarETag: this.user.avatarETag }), - }, - fields: { - msg: 'Room_members_list', - rids: [room._id], - type: 'room_member_list', - room: room.name || room.fname, - filters: filter, - }, - }); - - return API.v1.success({ - members, - count: members.length, - offset: skip, - total, - }); + response: { + 200: auditRoomMembersResponseSchema, + 400: validateBadRequestErrorResponse, + 401: validateUnauthorizedErrorResponse, + 403: validateForbiddenErrorResponse, + 404: validateNotFoundErrorResponse, }, }, + async function action() { + const { roomId, filter } = this.queryParams; + const { count: limit, offset: skip } = await getPaginationItems(this.queryParams); + const { sort } = await this.parseJsonQuery(); + + const room = await Rooms.findOneById>(roomId, { projection: { _id: 1, name: 1, fname: 1 } }); + if (!room) { + return API.v1.notFound(); + } + + const { cursor, totalCount } = findUsersOfRoom({ + rid: room._id, + filter, + skip, + limit, + ...(sort?.username && { sort: { username: sort.username } }), + }); + + const [members, total] = await Promise.all([cursor.toArray(), totalCount]); + + await AuditLog.insertOne({ + ts: new Date(), + results: total, + u: { + _id: this.user._id, + username: this.user.username, + name: this.user.name, + ...(this.user.avatarETag && { avatarETag: this.user.avatarETag }), + }, + fields: { + msg: 'Room_members_list', + rids: [room._id], + type: 'room_member_list', + room: room.name || room.fname, + filters: filter, + }, + }); + + return API.v1.success({ + members, + count: members.length, + offset: skip, + total, + }); + }, ); API.v1.get( diff --git a/apps/meteor/ee/server/api/chat.ts b/apps/meteor/ee/server/api/chat.ts index 55e567141f054..03afc99ce9ec4 100644 --- a/apps/meteor/ee/server/api/chat.ts +++ b/apps/meteor/ee/server/api/chat.ts @@ -1,5 +1,11 @@ import type { IMessage, IReadReceiptWithUser } from '@rocket.chat/core-typings'; import { License } from '@rocket.chat/license'; +import { + ajv, + isChatGetMessageReadReceiptsProps, + validateBadRequestErrorResponse, + validateUnauthorizedErrorResponse, +} from '@rocket.chat/rest-typings'; import { Meteor } from 'meteor/meteor'; import { API } from '../../../server/api/api'; @@ -20,28 +26,36 @@ declare module '@rocket.chat/rest-typings' { } } -API.v1.addRoute( +const getMessageReadReceiptsResponseSchema = ajv.compile<{ receipts: IReadReceiptWithUser[] }>({ + type: 'object', + properties: { + receipts: { type: 'array', items: { $ref: '#/components/schemas/IReadReceiptWithUser' } }, + success: { type: 'boolean', enum: [true] }, + }, + required: ['receipts', 'success'], + additionalProperties: false, +}); + +API.v1.get( 'chat.getMessageReadReceipts', { authRequired: true, - // license: ['message-read-receipt'] + query: isChatGetMessageReadReceiptsProps, + response: { + 200: getMessageReadReceiptsResponseSchema, + 400: validateBadRequestErrorResponse, + 401: validateUnauthorizedErrorResponse, + }, }, - { - async get() { - if (!License.hasModule('message-read-receipt')) { - throw new Meteor.Error('error-action-not-allowed', 'This is an enterprise feature'); - } + async function action() { + if (!License.hasModule('message-read-receipt')) { + throw new Meteor.Error('error-action-not-allowed', 'This is an enterprise feature'); + } - const { messageId } = this.queryParams; - if (!messageId) { - return API.v1.failure({ - error: "The required 'messageId' param is missing.", - }); - } + const { messageId } = this.queryParams; - return API.v1.success({ - receipts: await getReadReceiptsFunction(messageId, this.userId), - }); - }, + return API.v1.success({ + receipts: await getReadReceiptsFunction(messageId, this.userId), + }); }, ); diff --git a/apps/meteor/ee/server/api/engagementDashboard/channels.ts b/apps/meteor/ee/server/api/engagementDashboard/channels.ts index 58480e1f834de..5e550a86d6049 100644 --- a/apps/meteor/ee/server/api/engagementDashboard/channels.ts +++ b/apps/meteor/ee/server/api/engagementDashboard/channels.ts @@ -1,4 +1,10 @@ import type { IDirectMessageRoom, IRoom } from '@rocket.chat/core-typings'; +import { + ajv, + validateBadRequestErrorResponse, + validateUnauthorizedErrorResponse, + validateForbiddenErrorResponse, +} from '@rocket.chat/rest-typings'; import { check, Match } from 'meteor/check'; import { API } from '../../../../server/api'; @@ -14,7 +20,7 @@ declare module '@rocket.chat/rest-typings' { channels: { room: { _id: IRoom['_id']; - name: IRoom['name'] | IRoom['fname']; + name: IRoom['name']; ts: IRoom['ts']; t: IRoom['t']; _updatedAt: IRoom['_updatedAt']; @@ -32,40 +38,99 @@ declare module '@rocket.chat/rest-typings' { } } -API.v1.addRoute( +const channelsListResponseSchema = ajv.compile<{ + channels: { + room: { + _id: IRoom['_id']; + name: IRoom['name']; + ts: IRoom['ts']; + t: IRoom['t']; + _updatedAt: IRoom['_updatedAt']; + usernames?: IDirectMessageRoom['usernames']; + }; + messages: number; + lastWeekMessages: number; + diffFromLastWeek: number; + }[]; + count: number; + offset: number; + total: number; +}>({ + type: 'object', + properties: { + channels: { + type: 'array', + items: { + type: 'object', + properties: { + room: { + type: 'object', + properties: { + _id: { type: 'string' }, + name: { type: 'string' }, + ts: { type: 'string', format: 'date-time' }, + t: { type: 'string' }, + _updatedAt: { type: 'string', format: 'date-time' }, + usernames: { type: 'array', items: { type: 'string' } }, + }, + required: ['_id'], + additionalProperties: false, + }, + messages: { type: 'number' }, + lastWeekMessages: { type: 'number' }, + diffFromLastWeek: { type: 'number' }, + }, + required: ['room', 'messages', 'lastWeekMessages', 'diffFromLastWeek'], + additionalProperties: false, + }, + }, + count: { type: 'number' }, + offset: { type: 'number' }, + total: { type: 'number' }, + success: { type: 'boolean', enum: [true] }, + }, + required: ['channels', 'count', 'offset', 'total', 'success'], + additionalProperties: false, +}); + +API.v1.get( 'engagement-dashboard/channels/list', { authRequired: true, permissionsRequired: ['view-engagement-dashboard'], license: ['engagement-dashboard'], + response: { + 200: channelsListResponseSchema, + 400: validateBadRequestErrorResponse, + 401: validateUnauthorizedErrorResponse, + 403: validateForbiddenErrorResponse, + }, }, - { - async get() { - check( - this.queryParams, - Match.ObjectIncluding({ - start: Match.Where(isDateISOString), - end: Match.Where(isDateISOString), - offset: Match.Maybe(String), - count: Match.Maybe(String), - }), - ); + async function action() { + check( + this.queryParams, + Match.ObjectIncluding({ + start: Match.Where(isDateISOString), + end: Match.Where(isDateISOString), + offset: Match.Maybe(String), + count: Match.Maybe(String), + }), + ); - const { start, end } = this.queryParams; - const { offset, count } = await getPaginationItems(this.queryParams); + const { start, end } = this.queryParams; + const { offset, count } = await getPaginationItems(this.queryParams); - const { channels, total } = await findChannelsWithNumberOfMessages({ - start: mapDateForAPI(start), - end: mapDateForAPI(end), - options: { offset, count }, - }); + const { channels, total } = await findChannelsWithNumberOfMessages({ + start: mapDateForAPI(start), + end: mapDateForAPI(end), + options: { offset, count }, + }); - return API.v1.success({ - channels, - total, - offset, - count: channels.length, - }); - }, + return API.v1.success({ + channels, + total, + offset, + count: channels.length, + }); }, ); diff --git a/apps/meteor/ee/server/api/ldap.ts b/apps/meteor/ee/server/api/ldap.ts index fb2e15c15abb0..e0cd8eff809f3 100644 --- a/apps/meteor/ee/server/api/ldap.ts +++ b/apps/meteor/ee/server/api/ldap.ts @@ -1,37 +1,50 @@ import { LDAPEnterprise } from '@rocket.chat/core-services'; +import { ajv, validateBadRequestErrorResponse, validateUnauthorizedErrorResponse } from '@rocket.chat/rest-typings'; import { settings } from '../../../app/settings/server'; import { API } from '../../../server/api/api'; import { hasPermissionAsync } from '../../../server/lib/authorization/hasPermission'; -API.v1.addRoute( +const ldapSyncNowResponseSchema = ajv.compile<{ message: string }>({ + type: 'object', + properties: { + message: { type: 'string' }, + success: { type: 'boolean', enum: [true] }, + }, + required: ['message', 'success'], + additionalProperties: false, +}); + +API.v1.post( 'ldap.syncNow', { authRequired: true, forceTwoFactorAuthenticationForNonEnterprise: true, twoFactorRequired: true, - // license: ['ldap-enterprise'], + response: { + 200: ldapSyncNowResponseSchema, + 400: validateBadRequestErrorResponse, + 401: validateUnauthorizedErrorResponse, + }, }, - { - async post() { - if (!this.userId) { - throw new Error('error-invalid-user'); - } + async function action() { + if (!this.userId) { + throw new Error('error-invalid-user'); + } - if (!(await hasPermissionAsync(this.userId, 'sync-auth-services-users'))) { - throw new Error('error-not-authorized'); - } + if (!(await hasPermissionAsync(this.userId, 'sync-auth-services-users'))) { + throw new Error('error-not-authorized'); + } - if (settings.get('LDAP_Enable') !== true) { - throw new Error('LDAP_disabled'); - } + if (settings.get('LDAP_Enable') !== true) { + throw new Error('LDAP_disabled'); + } - await LDAPEnterprise.sync(); - await LDAPEnterprise.syncAvatarAndAbacAttributes(); + await LDAPEnterprise.sync(); + await LDAPEnterprise.syncAvatarAndAbacAttributes(); - return API.v1.success({ - message: 'Sync_in_progress' as const, - }); - }, + return API.v1.success({ + message: 'Sync_in_progress' as const, + }); }, ); diff --git a/packages/core-typings/src/Ajv.ts b/packages/core-typings/src/Ajv.ts index b910c9aa2f9ac..7d9a8cebbee81 100644 --- a/packages/core-typings/src/Ajv.ts +++ b/packages/core-typings/src/Ajv.ts @@ -16,6 +16,7 @@ import type { IMessage } from './IMessage'; import type { IModerationAudit, IModerationReport } from './IModerationReport'; import type { IOAuthApps } from './IOAuthApps'; import type { IPermission } from './IPermission'; +import type { IReadReceiptWithUser } from './IReadReceipt'; import type { IRole } from './IRole'; import type { IRoom, IDirectoryChannelResult, IRoomAdmin } from './IRoom'; import type { ISubscription } from './ISubscription'; @@ -58,6 +59,7 @@ export const schemas = typia.json.schemas< | IIntegration | IIntegrationHistory | IMeApiUser + | IReadReceiptWithUser ), CallHistoryItem, ICustomUserStatus,