From 7c8b4e5b116504a280d4acdb474791ec9f5fefce Mon Sep 17 00:00:00 2001 From: Tiago Evangelista Pinto Date: Mon, 22 Nov 2021 17:02:33 -0300 Subject: [PATCH] [FIX] New specific endpoint for contactChatHistoryMessages with right permissions (#23533) Co-authored-by: Kevin Aleman Co-authored-by: Guilherme Gazzo --- .../tabbar/contactChatHistoryMessages.html | 28 +++++--- .../app/tabbar/contactChatHistoryMessages.js | 24 +++++-- app/livechat/imports/server/rest/visitors.ts | 47 ++++++++++++ app/livechat/server/api.js | 1 + app/livechat/server/api/lib/visitors.js | 1 + app/models/server/raw/Messages.js | 13 ++++ .../Omnichannel/hooks/useAgentsList.ts | 2 +- .../Omnichannel/hooks/useDepartmentsList.ts | 2 +- .../views/admin/customEmoji/CustomEmoji.tsx | 2 +- definition/rest/helpers/PaginatedRequest.ts | 9 +-- definition/rest/helpers/PaginatedResult.ts | 4 +- definition/rest/v1/emojiCustom.ts | 6 +- definition/rest/v1/omnichannel.ts | 72 ++++++++++--------- .../server/api/departments.js | 10 +-- .../authorization/canAccessRoomLivechat.ts | 1 + 15 files changed, 153 insertions(+), 69 deletions(-) create mode 100644 app/livechat/imports/server/rest/visitors.ts diff --git a/app/livechat/client/views/app/tabbar/contactChatHistoryMessages.html b/app/livechat/client/views/app/tabbar/contactChatHistoryMessages.html index 1b5e7ec2b10..7b83a9db8de 100644 --- a/app/livechat/client/views/app/tabbar/contactChatHistoryMessages.html +++ b/app/livechat/client/views/app/tabbar/contactChatHistoryMessages.html @@ -33,17 +33,25 @@ {{else}} -
-
    - {{# with messageContext}} - {{#each msg in messages}}{{> message msg=msg room=room subscription=subscription settings=settings u=u}}{{/each}} - {{/with}} + {{#if hasError}} +
    +
    +

    {{_ "Not_found_or_not_allowed"}}

    +
    +
    + {{else}} +
    +
      + {{# with messageContext}} + {{#each msg in messages}}{{> message msg=msg room=room subscription=subscription settings=settings u=u}}{{/each}} + {{/with}} - {{#if isLoading}} - {{> loading}} - {{/if}} -
    -
    + {{#if isLoading}} + {{> loading}} + {{/if}} +
+
+ {{/if}} {{/if}} diff --git a/app/livechat/client/views/app/tabbar/contactChatHistoryMessages.js b/app/livechat/client/views/app/tabbar/contactChatHistoryMessages.js index c73136c07a8..cb3171f67c6 100644 --- a/app/livechat/client/views/app/tabbar/contactChatHistoryMessages.js +++ b/app/livechat/client/views/app/tabbar/contactChatHistoryMessages.js @@ -36,6 +36,12 @@ Template.contactChatHistoryMessages.helpers({ empty() { return Template.instance().messages.get().length === 0; }, + hasError() { + return Template.instance().hasError.get(); + }, + error() { + return Template.instance().error.get(); + }, }); Template.contactChatHistoryMessages.events({ @@ -72,15 +78,23 @@ Template.contactChatHistoryMessages.onCreated(function() { this.searchTerm = new ReactiveVar(''); this.isLoading = new ReactiveVar(true); this.limit = new ReactiveVar(MESSAGES_LIMIT); + this.hasError = new ReactiveVar(false); + this.error = new ReactiveVar(null); this.loadMessages = async (url) => { this.isLoading.set(true); const offset = this.offset.get(); - const { messages, total } = await APIClient.v1.get(url); - this.messages.set(offset === 0 ? messages : this.messages.get().concat(messages)); - this.hasMore.set(total > this.messages.get().length); - this.isLoading.set(false); + try { + const { messages, total } = await APIClient.v1.get(url); + this.messages.set(offset === 0 ? messages : this.messages.get().concat(messages)); + this.hasMore.set(total > this.messages.get().length); + } catch (e) { + this.hasError.set(true); + this.error.set(e); + } finally { + this.isLoading.set(false); + } }; this.autorun(() => { @@ -92,7 +106,7 @@ Template.contactChatHistoryMessages.onCreated(function() { return this.loadMessages(`chat.search/?roomId=${ this.rid }&searchText=${ searchTerm }&count=${ limit }&offset=${ offset }&sort={"ts": 1}`); } - this.loadMessages(`channels.messages/?roomId=${ this.rid }&count=${ limit }&offset=${ offset }&sort={"ts": 1}&query={"$or": [ {"t": {"$exists": false} }, {"t": "livechat-close"} ] }`); + this.loadMessages(`livechat/${ this.rid }/messages?count=${ limit }&offset=${ offset }&sort={"ts": 1}`); }); this.autorun(() => { diff --git a/app/livechat/imports/server/rest/visitors.ts b/app/livechat/imports/server/rest/visitors.ts new file mode 100644 index 00000000000..e75d4a955a2 --- /dev/null +++ b/app/livechat/imports/server/rest/visitors.ts @@ -0,0 +1,47 @@ + +import { check } from 'meteor/check'; + +import { API } from '../../../../api/server'; +import { LivechatRooms } from '../../../../models/server'; +import { Messages } from '../../../../models/server/raw'; +import { normalizeMessagesForUser } from '../../../../utils/server/lib/normalizeMessagesForUser'; +import { canAccessRoom } from '../../../../authorization/server'; +import { IMessage } from '../../../../../definition/IMessage'; + +API.v1.addRoute('livechat/:rid/messages', { authRequired: true, permissionsRequired: ['view-l-room'] }, { + async get() { + check(this.urlParams, { + rid: String, + }); + + const { offset, count } = this.getPaginationItems(); + const { sort } = this.parseJsonQuery(); + + const room = LivechatRooms.findOneById(this.urlParams.rid); + + if (!room) { + throw new Error('invalid-room'); + } + + if (!canAccessRoom(room, this.user)) { + throw new Error('not-allowed'); + } + + const cursor = Messages.findLivechatClosedMessages(this.urlParams.rid, { + sort: sort || { ts: -1 }, + skip: offset, + limit: count, + }); + + const total = await cursor.count(); + + const messages = await cursor.toArray() as IMessage[]; + + return API.v1.success({ + messages: normalizeMessagesForUser(messages, this.userId), + offset, + count, + total, + }); + }, +}); diff --git a/app/livechat/server/api.js b/app/livechat/server/api.js index 6a13dddc86b..7aa0ee39c4a 100644 --- a/app/livechat/server/api.js +++ b/app/livechat/server/api.js @@ -11,6 +11,7 @@ import '../imports/server/rest/triggers.js'; import '../imports/server/rest/integrations.js'; import '../imports/server/rest/messages.js'; import '../imports/server/rest/visitors.js'; +import '../imports/server/rest/visitors.ts'; import '../imports/server/rest/dashboards.js'; import '../imports/server/rest/queue.js'; import '../imports/server/rest/officeHour.js'; diff --git a/app/livechat/server/api/lib/visitors.js b/app/livechat/server/api/lib/visitors.js index aec58e18db4..d03566d6da9 100644 --- a/app/livechat/server/api/lib/visitors.js +++ b/app/livechat/server/api/lib/visitors.js @@ -72,6 +72,7 @@ export async function findChatHistory({ userId, roomId, visitorId, pagination: { total, }; } + export async function searchChats({ userId, roomId, visitorId, searchText, closedChatsOnly, servedChatsOnly: served, pagination: { offset, count, sort } }) { if (!await hasPermissionAsync(userId, 'view-l-room')) { throw new Error('error-not-authorized'); diff --git a/app/models/server/raw/Messages.js b/app/models/server/raw/Messages.js index 06addaca1cd..f3704d6a53b 100644 --- a/app/models/server/raw/Messages.js +++ b/app/models/server/raw/Messages.js @@ -184,4 +184,17 @@ export class MessagesRaw extends BaseRaw { } return this.col.aggregate(params).toArray(); } + + findLivechatClosedMessages(rid, options) { + return this.find( + { + rid, + $or: [ + { t: { $exists: false } }, + { t: 'livechat-close' }, + ], + }, + options, + ); + } } diff --git a/client/components/Omnichannel/hooks/useAgentsList.ts b/client/components/Omnichannel/hooks/useAgentsList.ts index 4b3b894ebc5..5c01de2c896 100644 --- a/client/components/Omnichannel/hooks/useAgentsList.ts +++ b/client/components/Omnichannel/hooks/useAgentsList.ts @@ -37,7 +37,7 @@ export const useAgentsList = ( ...(options.text && { text: options.text }), offset: start, count: end + start, - sort: JSON.stringify({ name: 1 }), + sort: `{ name: 1 }`, }); const items = agents.map((agent: any) => { diff --git a/client/components/Omnichannel/hooks/useDepartmentsList.ts b/client/components/Omnichannel/hooks/useDepartmentsList.ts index 5447c37d187..9b6b8e46a25 100644 --- a/client/components/Omnichannel/hooks/useDepartmentsList.ts +++ b/client/components/Omnichannel/hooks/useDepartmentsList.ts @@ -40,7 +40,7 @@ export const useDepartmentsList = ( text: options.filter, offset: start, count: end + start, - sort: JSON.stringify({ name: 1 }), + sort: `{ name: 1 }`, }); const items = departments diff --git a/client/views/admin/customEmoji/CustomEmoji.tsx b/client/views/admin/customEmoji/CustomEmoji.tsx index 28db8f4b4ce..c8b40a477d5 100644 --- a/client/views/admin/customEmoji/CustomEmoji.tsx +++ b/client/views/admin/customEmoji/CustomEmoji.tsx @@ -42,7 +42,7 @@ const CustomEmoji: FC = function CustomEmoji({ onClick, reload useMemo( () => ({ query: JSON.stringify({ name: { $regex: text || '', $options: 'i' } }), - sort: JSON.stringify({ [sortBy]: sortDirection === 'asc' ? 1 : -1 }), + sort: `{ ${sortBy}: ${sortDirection === 'asc' ? 1 : -1} }`, count: itemsPerPage, offset: current, }), diff --git a/definition/rest/helpers/PaginatedRequest.ts b/definition/rest/helpers/PaginatedRequest.ts index 44962dea206..edaa045a559 100644 --- a/definition/rest/helpers/PaginatedRequest.ts +++ b/definition/rest/helpers/PaginatedRequest.ts @@ -1,4 +1,5 @@ -export type PaginatedRequest = { - count: number; - offset: number; -}; +export type PaginatedRequest = { + count?: number; + offset?: number; + sort?: `{ ${S}: ${1 | -1} }` | string; +} & T; diff --git a/definition/rest/helpers/PaginatedResult.ts b/definition/rest/helpers/PaginatedResult.ts index e78980c0d1e..ea153093cee 100644 --- a/definition/rest/helpers/PaginatedResult.ts +++ b/definition/rest/helpers/PaginatedResult.ts @@ -1,5 +1,5 @@ -export type PaginatedResult = { +export type PaginatedResult = { count: number; offset: number; total: number; -}; +} & T; diff --git a/definition/rest/v1/emojiCustom.ts b/definition/rest/v1/emojiCustom.ts index 89963edc429..8ef956e5acd 100644 --- a/definition/rest/v1/emojiCustom.ts +++ b/definition/rest/v1/emojiCustom.ts @@ -4,11 +4,7 @@ import { PaginatedResult } from '../helpers/PaginatedResult'; export type EmojiCustomEndpoints = { 'emoji-custom.all': { - GET: ( - params: { query: string } & PaginatedRequest & { - sort: string; // {name: 'asc' | 'desc';}>; - }, - ) => { + GET: (params: PaginatedRequest<{ query: string }, 'name'>) => { emojis: ICustomEmojiDescriptor[]; } & PaginatedResult; }; diff --git a/definition/rest/v1/omnichannel.ts b/definition/rest/v1/omnichannel.ts index a2d43915ab0..1624a4be26a 100644 --- a/definition/rest/v1/omnichannel.ts +++ b/definition/rest/v1/omnichannel.ts @@ -1,8 +1,11 @@ import { ILivechatDepartment } from '../../ILivechatDepartment'; import { ILivechatMonitor } from '../../ILivechatMonitor'; import { ILivechatTag } from '../../ILivechatTag'; +import { IMessage } from '../../IMessage'; import { IOmnichannelRoom, IRoom } from '../../IRoom'; import { ISetting } from '../../ISetting'; +import { PaginatedRequest } from '../helpers/PaginatedRequest'; +import { PaginatedResult } from '../helpers/PaginatedResult'; export type OmnichannelEndpoints = { 'livechat/appearance': { @@ -23,55 +26,55 @@ export type OmnichannelEndpoints = { POST: (params: { roomId: IRoom['_id'] }) => void; }; 'livechat/monitors.list': { - GET: (params: { text: string; offset: number; count: number }) => { + GET: (params: PaginatedRequest<{ text: string }>) => PaginatedResult<{ monitors: ILivechatMonitor[]; - total: number; - }; + }>; }; 'livechat/tags.list': { - GET: (params: { text: string; offset: number; count: number }) => { + GET: (params: PaginatedRequest<{ text: string }, 'name'>) => PaginatedResult<{ tags: ILivechatTag[]; - total: number; - }; + }>; }; 'livechat/department': { - GET: (params: { - text: string; - offset?: number; - count?: number; - sort?: string; - onlyMyDepartments?: boolean; - }) => { + GET: ( + params: PaginatedRequest<{ + text: string; + onlyMyDepartments?: boolean; + }>, + ) => PaginatedResult<{ departments: ILivechatDepartment[]; - total: number; - }; + }>; }; 'livechat/department/:_id': { GET: () => { department: ILivechatDepartment; }; }; - 'livechat/departments.by-unit/:id': { - GET: (params: { text: string; offset: number; count: number }) => { + 'livechat/departments.available-by-unit/:id': { + GET: (params: PaginatedRequest<{ text: string }>) => PaginatedResult<{ departments: ILivechatDepartment[]; - total: number; - }; + }>; }; - 'livechat/departments.available-by-unit/:id': { - GET: (params: { text: string; offset: number; count: number }) => { + 'livechat/departments.by-unit/': { + GET: (params: PaginatedRequest<{ text: string }>) => PaginatedResult<{ departments: ILivechatDepartment[]; - total: number; - }; + }>; + }; + + 'livechat/departments.by-unit/:id': { + GET: (params: PaginatedRequest<{ text: string }>) => PaginatedResult<{ + departments: ILivechatDepartment[]; + }>; }; 'livechat/custom-fields': { - GET: () => { + GET: (params: PaginatedRequest<{ text: string }>) => PaginatedResult<{ customFields: [ { _id: string; label: string; }, ]; - }; + }>; }; 'livechat/rooms': { GET: (params: { @@ -86,15 +89,17 @@ export type OmnichannelEndpoints = { current: number; itemsPerPage: number; tags: string[]; - }) => { + }) => PaginatedResult<{ rooms: IOmnichannelRoom[]; - count: number; - offset: number; - total: number; - }; + }>; + }; + 'livechat/:rid/messages': { + GET: (params: PaginatedRequest<{ query: string }>) => PaginatedResult<{ + messages: IMessage[]; + }>; }; 'livechat/users/agent': { - GET: (params: { text?: string; offset?: number; count?: number; sort?: string }) => { + GET: (params: PaginatedRequest<{ text?: string }>) => PaginatedResult<{ users: { _id: string; emails: { @@ -109,9 +114,6 @@ export type OmnichannelEndpoints = { maxNumberSimultaneousChat: number; }; }[]; - count: number; - offset: number; - total: number; - }; + }>; }; }; diff --git a/ee/app/livechat-enterprise/server/api/departments.js b/ee/app/livechat-enterprise/server/api/departments.js index 49c9e840d3e..07c7c5a6c68 100644 --- a/ee/app/livechat-enterprise/server/api/departments.js +++ b/ee/app/livechat-enterprise/server/api/departments.js @@ -344,15 +344,15 @@ API.v1.addRoute('livechat/departments.available-by-unit/:unitId', { authRequired }, }); -API.v1.addRoute('livechat/departments.by-unit/:unitId', { authRequired: true }, { - get() { +API.v1.addRoute('livechat/departments.by-unit/:id', { authRequired: true }, { + async get() { check(this.urlParams, { - unitId: String, + id: String, }); const { offset, count } = this.getPaginationItems(); - const { unitId } = this.urlParams; + const { id } = this.urlParams; - const { departments, total } = Promise.await(findAllDepartmentsByUnit(unitId, offset, count)); + const { departments, total } = await findAllDepartmentsByUnit(id, offset, count); return API.v1.success({ departments, diff --git a/server/services/authorization/canAccessRoomLivechat.ts b/server/services/authorization/canAccessRoomLivechat.ts index a0dd92acbb1..0418d105624 100644 --- a/server/services/authorization/canAccessRoomLivechat.ts +++ b/server/services/authorization/canAccessRoomLivechat.ts @@ -9,6 +9,7 @@ export const canAccessRoomLivechat: RoomAccessValidator = async (room, user, ext // room can be sent as `null` but in that case a `rid` is also sent on extraData // this is the case for file uploads const livechatRoom = room || (extraData?.rid && await Rooms.findOneById(extraData?.rid)); + if (livechatRoom?.t !== 'l') { return false; }