diff --git a/app/livechat/client/views/app/livechatAgents.js b/app/livechat/client/views/app/livechatAgents.js index e973f3902cb6b..f1f515addfde4 100644 --- a/app/livechat/client/views/app/livechatAgents.js +++ b/app/livechat/client/views/app/livechatAgents.js @@ -6,20 +6,21 @@ import s from 'underscore.string'; import _ from 'underscore'; import { modal, call } from '../../../../ui-utils'; -import { t, handleError } from '../../../../utils'; -import { AgentUsers } from '../../collections/AgentUsers'; - - +import { t, handleError, APIClient } from '../../../../utils/client'; import './livechatAgents.html'; +const loadAgents = async (instance, limit = 50) => { + const { users } = await APIClient.v1.get(`livechat/users/agent?count=${ limit }`); + instance.agents.set(users); + instance.ready.set(true); +}; + const getUsername = (user) => user.username; Template.livechatAgents.helpers({ exceptionsAgents() { const { selectedAgents } = Template.instance(); - return AgentUsers.find({}, { fields: { username: 1 } }) - .fetch() - .map(getUsername) - .concat(selectedAgents.get().map(getUsername)); + return Template.instance().agents.get() + .map(getUsername).concat(selectedAgents.get().map(getUsername)); }, deleteLastAgent() { const i = Template.instance(); @@ -33,7 +34,7 @@ Template.livechatAgents.helpers({ return Template.instance().state.get('loading'); }, agents() { - return Template.instance().agents(); + return Template.instance().getAgentsWithCriteria(); }, emailAddress() { if (this.emails && this.emails.length > 0) { @@ -82,7 +83,7 @@ Template.livechatAgents.helpers({ const DEBOUNCE_TIME_FOR_SEARCH_AGENTS_IN_MS = 300; Template.livechatAgents.events({ - 'click .remove-agent'(e /* , instance*/) { + 'click .remove-agent'(e, instance) { e.preventDefault(); modal.open( @@ -97,12 +98,13 @@ Template.livechatAgents.events({ html: false, }, () => { - Meteor.call('livechat:removeAgent', this.username, function( + Meteor.call('livechat:removeAgent', this.username, async function( error /* , result*/ ) { if (error) { return handleError(error); } + await loadAgents(instance); modal.open({ title: t('Removed'), text: t('Agent_removed'), @@ -130,6 +132,7 @@ Template.livechatAgents.events({ await Promise.all( users.map(({ username }) => call('livechat:addAgent', username)) ); + await loadAgents(instance); selectedAgents.set([]); } finally { state.set('loading', false); @@ -171,6 +174,7 @@ Template.livechatAgents.onCreated(function() { }); this.ready = new ReactiveVar(true); this.selectedAgents = new ReactiveVar([]); + this.agents = new ReactiveVar([]); this.onSelectAgents = ({ item: agent }) => { this.selectedAgents.set([...this.selectedAgents.curValue, agent]); @@ -181,25 +185,19 @@ Template.livechatAgents.onCreated(function() { }; this.autorun(function() { - const filter = instance.filter.get(); const limit = instance.limit.get(); - const subscription = instance.subscribe('livechat:agents', filter, limit); - instance.ready.set(subscription.ready()); + loadAgents(instance, limit); }); - this.agents = function() { + this.getAgentsWithCriteria = function() { let filter; - let query = {}; if (instance.filter && instance.filter.get()) { filter = s.trim(instance.filter.get()); } - - if (filter) { - const filterReg = new RegExp(s.escapeRegExp(filter), 'i'); - query = { $or: [{ username: filterReg }, { name: filterReg }, { 'emails.address': filterReg }] }; - } - - const limit = instance.limit && instance.limit.get(); - return AgentUsers.find(query, { limit, sort: { name: 1 } }).fetch(); + const regex = new RegExp(s.escapeRegExp(filter), 'i'); + return instance.agents.get() + .filter((agent) => agent.name.match(regex) + || agent.username.match(regex) + || agent.emails.some((email) => email.address.match(regex))); }; }); diff --git a/app/livechat/client/views/app/livechatDepartmentForm.js b/app/livechat/client/views/app/livechatDepartmentForm.js index 1a30e4485e049..aee681dadc1a0 100644 --- a/app/livechat/client/views/app/livechatDepartmentForm.js +++ b/app/livechat/client/views/app/livechatDepartmentForm.js @@ -7,7 +7,6 @@ import toastr from 'toastr'; import { t, handleError } from '../../../../utils'; import { hasPermission } from '../../../../authorization'; -import { AgentUsers } from '../../collections/AgentUsers'; import { getCustomFormTemplate } from './customTemplates/register'; import './livechatDepartmentForm.html'; import { APIClient } from '../../../../utils/client'; @@ -22,10 +21,6 @@ Template.livechatDepartmentForm.helpers({ selectedAgents() { return _.sortBy(Template.instance().selectedAgents.get(), 'username'); }, - availableAgents() { - const selected = _.pluck(Template.instance().selectedAgents.get(), 'username'); - return AgentUsers.find({ username: { $nin: selected } }, { sort: { username: 1 } }); - }, showOnRegistration(value) { const department = Template.instance().department.get(); return department.showOnRegistration === value || (department.showOnRegistration === undefined && value === true); @@ -147,7 +142,7 @@ Template.livechatDepartmentForm.events({ } input.value = ''; - const agent = AgentUsers.findOne({ username }); + const agent = Template.instance().agents.get().find((agent) => agent.username === username); if (!agent) { return toastr.error(t('The_selected_user_is_not_an_agent')); } @@ -182,11 +177,13 @@ Template.livechatDepartmentForm.events({ }, }); -Template.livechatDepartmentForm.onCreated(function() { +Template.livechatDepartmentForm.onCreated(async function() { this.department = new ReactiveVar({ enabled: true }); this.selectedAgents = new ReactiveVar([]); + this.agents = new ReactiveVar([]); - this.subscribe('livechat:agents'); + const { users } = await APIClient.v1.get('livechat/users/agent'); + this.agents.set(users); this.autorun(async () => { const id = FlowRouter.getParam('_id'); diff --git a/app/livechat/client/views/app/livechatQueue.js b/app/livechat/client/views/app/livechatQueue.js index 7182ef8dfa320..e7bc76c7c9c5b 100644 --- a/app/livechat/client/views/app/livechatQueue.js +++ b/app/livechat/client/views/app/livechatQueue.js @@ -6,7 +6,6 @@ import { settings } from '../../../../settings'; import { hasPermission } from '../../../../authorization'; import { Users } from '../../../../models'; import { LivechatQueueUser } from '../../collections/LivechatQueueUser'; -import { AgentUsers } from '../../collections/AgentUsers'; import './livechatQueue.html'; import { APIClient } from '../../../../utils/client'; @@ -31,9 +30,9 @@ Template.livechatQueue.helpers({ }).forEach((user) => { const options = { fields: { _id: 1 } }; const userFilter = { _id: user.agentId, status: { $ne: 'offline' } }; - const agentFilter = { _id: user.agentId, statusLivechat: 'available' }; + const agent = Template.instance().agents.get().find((agent) => agent._id === user.agentId && agent.statusLivechat === 'available'); - if (showOffline[this._id] || (Meteor.users.findOne(userFilter, options) && AgentUsers.findOne(agentFilter, options))) { + if (showOffline[this._id] || (Meteor.users.findOne(userFilter, options) && agent)) { users.push(user); } }); @@ -59,10 +58,13 @@ Template.livechatQueue.events({ Template.livechatQueue.onCreated(async function() { this.showOffline = new ReactiveVar({}); + this.agents = new ReactiveVar([]); this.departments = new ReactiveVar([]); this.subscribe('livechat:queue'); - this.subscribe('livechat:agents'); + const { users } = await APIClient.v1.get('livechat/users/agent'); const { departments } = await APIClient.v1.get('livechat/department?sort={"name": 1}'); + + this.agents.set(users); this.departments.set(departments); }); diff --git a/app/livechat/client/views/app/tabbar/visitorForward.js b/app/livechat/client/views/app/tabbar/visitorForward.js index 75ab0417585ae..ecd93e115d59e 100644 --- a/app/livechat/client/views/app/tabbar/visitorForward.js +++ b/app/livechat/client/views/app/tabbar/visitorForward.js @@ -6,7 +6,6 @@ import toastr from 'toastr'; import { ChatRoom } from '../../../../../models'; import { t } from '../../../../../utils'; -import { AgentUsers } from '../../../collections/AgentUsers'; import './visitorForward.html'; import { APIClient } from '../../../../../utils/client'; @@ -21,13 +20,8 @@ Template.visitorForward.helpers({ return Template.instance().departments.get().filter((department) => department.enabled === true); }, agents() { - const query = { - _id: { $ne: Meteor.userId() }, - status: { $ne: 'offline' }, - statusLivechat: 'available', - }; - - return AgentUsers.find(query, { sort: { name: 1, username: 1 } }); + return Template.instance().agents.get() + .filter((agent) => agent._id !== Meteor.userId() && agent.status !== 'offline' && agent.statusLivechat === 'available'); }, agentName() { return this.name || this.username; @@ -37,6 +31,7 @@ Template.visitorForward.helpers({ Template.visitorForward.onCreated(async function() { this.visitor = new ReactiveVar(); this.room = new ReactiveVar(); + this.agents = new ReactiveVar([]); this.departments = new ReactiveVar([]); this.autorun(() => { @@ -46,8 +41,10 @@ Template.visitorForward.onCreated(async function() { this.autorun(() => { this.room.set(ChatRoom.findOne({ _id: Template.currentData().roomId })); }); - this.subscribe('livechat:agents'); + + const { users } = await APIClient.v1.get('livechat/users/agent?sort={"name": 1, "username": 1}'); const { departments } = await APIClient.v1.get('livechat/department'); + this.agents.set(users); this.departments.set(departments); }); diff --git a/app/livechat/imports/server/rest/users.js b/app/livechat/imports/server/rest/users.js index 5730b46f22890..179cb61379255 100644 --- a/app/livechat/imports/server/rest/users.js +++ b/app/livechat/imports/server/rest/users.js @@ -1,39 +1,41 @@ import { check } from 'meteor/check'; import _ from 'underscore'; -import { hasPermission, getUsersInRole } from '../../../../authorization'; +import { hasPermission } from '../../../../authorization'; import { API } from '../../../../api'; import { Users } from '../../../../models'; import { Livechat } from '../../../server/lib/Livechat'; +import { findAgents, findManagers } from '../../../server/api/lib/users'; API.v1.addRoute('livechat/users/:type', { authRequired: true }, { get() { - if (!hasPermission(this.userId, 'view-livechat-manager')) { - return API.v1.unauthorized(); + check(this.urlParams, { + type: String, + }); + const { offset, count } = this.getPaginationItems(); + const { sort } = this.parseJsonQuery(); + + if (this.urlParams.type === 'agent') { + return API.v1.success(Promise.await(findAgents({ + userId: this.userId, + pagination: { + offset, + count, + sort, + }, + }))); } - - try { - check(this.urlParams, { - type: String, - }); - - let role; - if (this.urlParams.type === 'agent') { - role = 'livechat-agent'; - } else if (this.urlParams.type === 'manager') { - role = 'livechat-manager'; - } else { - throw new Error('Invalid type'); - } - - const users = getUsersInRole(role); - - return API.v1.success({ - users: users.fetch().map((user) => _.pick(user, '_id', 'username', 'name', 'status', 'statusLivechat')), - }); - } catch (e) { - return API.v1.failure(e.error); + if (this.urlParams.type === 'manager') { + return API.v1.success(Promise.await(findManagers({ + userId: this.userId, + pagination: { + offset, + count, + sort, + }, + }))); } + throw new Error('Invalid type'); }, post() { if (!hasPermission(this.userId, 'view-livechat-manager')) { diff --git a/app/livechat/server/api/lib/users.js b/app/livechat/server/api/lib/users.js new file mode 100644 index 0000000000000..0fb8f1d307bc7 --- /dev/null +++ b/app/livechat/server/api/lib/users.js @@ -0,0 +1,55 @@ +import { hasPermissionAsync } from '../../../../authorization/server/functions/hasPermission'; +import { Users } from '../../../../models/server/raw'; + +async function findUsers({ userId, role, pagination: { offset, count, sort } }) { + if (!await hasPermissionAsync(userId, 'view-livechat-manager') || !await hasPermissionAsync(userId, 'manage-livechat-agents')) { + throw new Error('error-not-authorized'); + } + + const cursor = await Users.findUsersInRoles(role, undefined, { + sort: sort || { name: 1 }, + skip: offset, + limit: count, + fields: { + username: 1, + name: 1, + status: 1, + statusLivechat: 1, + emails: 1, + }, + }); + + const total = await cursor.count(); + + const users = await cursor.toArray(); + + return { + users, + count: users.length, + offset, + total, + }; +} +export async function findAgents({ userId, pagination: { offset, count, sort } }) { + return findUsers({ + role: 'livechat-agent', + userId, + pagination: { + offset, + count, + sort, + }, + }); +} + +export async function findManagers({ userId, pagination: { offset, count, sort } }) { + return findUsers({ + role: 'livechat-manager', + userId, + pagination: { + offset, + count, + sort, + }, + }); +} diff --git a/app/livechat/server/publications/livechatDepartments.js b/app/livechat/server/publications/livechatDepartments.js index b2b843991db9b..7c3a891a501aa 100644 --- a/app/livechat/server/publications/livechatDepartments.js +++ b/app/livechat/server/publications/livechatDepartments.js @@ -5,11 +5,11 @@ import { LivechatDepartment } from '../../../models'; Meteor.publish('livechat:departments', function(_id, limit = 50) { if (!this.userId) { - return this.error(new Meteor.Error('error-not-authorized', 'Not authorized', { publish: 'livechat:agents' })); + return this.error(new Meteor.Error('error-not-authorized', 'Not authorized', { publish: 'livechat:departments' })); } if (!hasPermission(this.userId, 'view-l-room')) { - return this.error(new Meteor.Error('error-not-authorized', 'Not authorized', { publish: 'livechat:agents' })); + return this.error(new Meteor.Error('error-not-authorized', 'Not authorized', { publish: 'livechat:departments' })); } if (_id) { diff --git a/app/livechat/server/publications/livechatOfficeHours.js b/app/livechat/server/publications/livechatOfficeHours.js index 42b964eeadab4..75739105e2860 100644 --- a/app/livechat/server/publications/livechatOfficeHours.js +++ b/app/livechat/server/publications/livechatOfficeHours.js @@ -5,7 +5,7 @@ import { LivechatOfficeHour } from '../../../models'; Meteor.publish('livechat:officeHour', function() { if (!hasPermission(this.userId, 'view-l-room')) { - return this.error(new Meteor.Error('error-not-authorized', 'Not authorized', { publish: 'livechat:agents' })); + return this.error(new Meteor.Error('error-not-authorized', 'Not authorized', { publish: 'livechat:officeHour' })); } return LivechatOfficeHour.find(); diff --git a/app/models/server/raw/BaseRaw.js b/app/models/server/raw/BaseRaw.js index b4bfc512523de..fd6eb9f067df6 100644 --- a/app/models/server/raw/BaseRaw.js +++ b/app/models/server/raw/BaseRaw.js @@ -11,11 +11,11 @@ export class BaseRaw { return this.col.findOne(...args); } - find(...args) { - return this.col.find(...args); - } - findUsersInRoles() { throw new Error('overwrite-function', 'You must overwrite this function in the extended classes'); } + + find(...args) { + return this.col.find(...args); + } } diff --git a/tests/data/livechat/rooms.js b/tests/data/livechat/rooms.js index 463ea08b5d198..75c73d4112181 100644 --- a/tests/data/livechat/rooms.js +++ b/tests/data/livechat/rooms.js @@ -2,7 +2,7 @@ import { api, credentials, request } from '../api-data'; import { adminUsername } from '../user'; export const createLivechatRoom = (visitorToken) => new Promise((resolve) => { - request.get(api(`/livechat/room?token=${ visitorToken }`)) + request.get(api(`livechat/room?token=${ visitorToken }`)) .set(credentials) .end((err, res) => resolve(res.body.room)); }); @@ -36,3 +36,12 @@ export const createAgent = () => new Promise((resolve) => { }) .end((err, res) => resolve(res.body.user)); }); + +export const createManager = () => new Promise((resolve) => { + request.post(api('livechat/users/manager')) + .set(credentials) + .send({ + username: adminUsername, + }) + .end((err, res) => resolve(res.body.user)); +}); diff --git a/tests/end-to-end/api/livechat/agents.js b/tests/end-to-end/api/livechat/agents.js new file mode 100644 index 0000000000000..5e265a2133ba6 --- /dev/null +++ b/tests/end-to-end/api/livechat/agents.js @@ -0,0 +1,97 @@ +import { getCredentials, api, request, credentials } from '../../../data/api-data.js'; +import { createAgent, createManager } from '../../../data/livechat/rooms.js'; +import { updatePermission, updateSetting } from '../../../data/permissions.helper'; + +describe('LIVECHAT - Agents', function() { + this.retries(0); + let agent; + let manager; + + before((done) => getCredentials(done)); + + before((done) => { + updateSetting('Livechat_enabled', true) + .then(createAgent) + .then((createdAgent) => { + agent = createdAgent; + }) + .then(createManager) + .then((createdManager) => { + manager = createdManager; + done(); + }); + }); + + describe('livechat/users/:type', () => { + it('should return an "unauthorized error" when the user does not have the necessary permission', (done) => { + updatePermission('view-livechat-manager', []) + .then(() => updatePermission('manage-livechat-agents', [])) + .then(() => { + request.get(api('livechat/users/agent')) + .set(credentials) + .expect('Content-Type', 'application/json') + .expect(400) + .expect((res) => { + expect(res.body).to.have.property('success', false); + expect(res.body.error).to.be.equal('error-not-authorized'); + }) + .end(done); + }); + }); + it('should throw an error when the type is invalid', (done) => { + updatePermission('view-livechat-manager', ['admin']) + .then(() => updatePermission('manage-livechat-agents', ['admin'])) + .then(() => { + request.get(api('livechat/users/invalid-type')) + .set(credentials) + .expect('Content-Type', 'application/json') + .expect(400) + .expect((res) => { + expect(res.body).to.have.property('success', false); + expect(res.body.error).to.be.equal('Invalid type'); + }) + .end(done); + }); + }); + it('should return an array of agents', (done) => { + updatePermission('view-livechat-manager', ['admin']) + .then(() => updatePermission('manage-livechat-agents', ['admin'])) + .then(() => { + request.get(api('livechat/users/agent')) + .set(credentials) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body.users).to.be.an('array'); + expect(res.body).to.have.property('offset'); + expect(res.body).to.have.property('total'); + expect(res.body).to.have.property('count'); + const agentRecentlyCreated = res.body.users.find((user) => agent._id === user._id); + expect(agentRecentlyCreated._id).to.be.equal(agent._id); + }) + .end(done); + }); + }); + it('should return an array of managers', (done) => { + updatePermission('view-livechat-manager', ['admin']) + .then(() => updatePermission('manage-livechat-agents', ['admin'])) + .then(() => { + request.get(api('livechat/users/manager')) + .set(credentials) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body.users).to.be.an('array'); + expect(res.body).to.have.property('offset'); + expect(res.body).to.have.property('total'); + expect(res.body).to.have.property('count'); + const managerRecentlyCreated = res.body.users.find((user) => manager._id === user._id); + expect(managerRecentlyCreated._id).to.be.equal(manager._id); + }) + .end(done); + }); + }); + }); +});