Skip to content
Merged
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
8 changes: 7 additions & 1 deletion apps/meteor/app/api/server/api.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ type Options = (
twoFactorOptions?: ITwoFactorOptions;
}
) & {
validateParams?: ValidateFunction;
validateParams?: ValidateFunction | { [key in Method]?: ValidateFunction };
authOrAnonRequired?: true;
};

Expand All @@ -93,6 +93,8 @@ type ActionThis<TMethod extends Method, TPathPattern extends PathPattern, TOptio
// TODO make it unsafe
readonly queryParams: TMethod extends 'GET'
? TOptions extends { validateParams: ValidateFunction<infer T> }
? T
: TOptions extends { validateParams: { GET: ValidateFunction<infer T> } }
? T
: Partial<OperationParams<TMethod, TPathPattern>>
: Record<string, string>;
Expand All @@ -101,6 +103,10 @@ type ActionThis<TMethod extends Method, TPathPattern extends PathPattern, TOptio
? Record<string, unknown>
: TOptions extends { validateParams: ValidateFunction<infer T> }
? T
: TOptions extends { validateParams: infer V }
? V extends { [key in TMethod]: ValidateFunction<infer T> }
? T
: Partial<OperationParams<TMethod, TPathPattern>>
: Partial<OperationParams<TMethod, TPathPattern>>;
readonly request: Request;

Expand Down
11 changes: 9 additions & 2 deletions apps/meteor/app/api/server/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -425,9 +425,16 @@ export class APIClass extends Restivus {
try {
api.enforceRateLimit(objectForRateLimitMatch, this.request, this.response, this.userId);

if (_options.validateParams && !_options.validateParams(this.request.method === 'GET' ? this.queryParams : this.bodyParams)) {
throw new Meteor.Error('invalid-params', _options.validateParams.errors?.map((error) => error.message).join('\n '));
if (_options.validateParams) {
const requestMethod = this.request.method;
const validatorFunc =
typeof _options.validateParams === 'function' ? _options.validateParams : _options.validateParams[requestMethod];

if (validatorFunc && !validatorFunc(requestMethod === 'GET' ? this.queryParams : this.bodyParams)) {
throw new Meteor.Error('invalid-params', validatorFunc.errors?.map((error) => error.message).join('\n '));
}
}

if (shouldVerifyPermissions && (!this.userId || !hasAllPermission(this.userId, _options.permissionsRequired))) {
throw new Meteor.Error('error-unauthorized', 'User does not have the permissions required for this action', {
permissions: _options.permissionsRequired,
Expand Down
101 changes: 49 additions & 52 deletions apps/meteor/app/livechat/imports/server/rest/departments.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { isLivechatDepartmentProps } from '@rocket.chat/rest-typings';
import { isGETLivechatDepartmentProps, isPOSTLivechatDepartmentProps } from '@rocket.chat/rest-typings';
import { Match, check } from 'meteor/check';

import { API } from '../../../../api/server';
Expand All @@ -15,7 +15,7 @@ import {

API.v1.addRoute(
'livechat/department',
{ authRequired: true, validateParams: isLivechatDepartmentProps },
{ authRequired: true, validateParams: { GET: isGETLivechatDepartmentProps, POST: isPOSTLivechatDepartmentProps } },
{
async get() {
if (!hasAtLeastOnePermission(this.userId, ['view-livechat-departments', 'view-l-room'])) {
Expand All @@ -27,50 +27,44 @@ API.v1.addRoute(

const { text, enabled, onlyMyDepartments, excludeDepartmentId } = this.queryParams;

const { departments, total } = Promise.await(
findDepartments({
userId: this.userId,
text,
enabled: enabled === 'true',
onlyMyDepartments: onlyMyDepartments === 'true',
excludeDepartmentId,
pagination: {
offset,
count,
// IMO, sort type shouldn't be record, but a generic of the model we're trying to sort
// or the form { [k: keyof T]: number | string }
sort: sort as any,
},
}),
);
const { departments, total } = await findDepartments({
userId: this.userId,
text,
enabled: enabled === 'true',
onlyMyDepartments: onlyMyDepartments === 'true',
excludeDepartmentId,
pagination: {
offset,
count,
// IMO, sort type shouldn't be record, but a generic of the model we're trying to sort
// or the form { [k: keyof T]: number | string }
sort: sort as any,
},
});

return API.v1.success({ departments, count: departments.length, offset, total });
},
post() {
async post() {
if (!hasPermission(this.userId, 'manage-livechat-departments')) {
return API.v1.unauthorized();
}

try {
check(this.bodyParams, {
department: Object,
agents: Match.Maybe(Array),
});
check(this.bodyParams, {
department: Object,
agents: Match.Maybe(Array),
});

const agents = this.bodyParams.agents ? { upsert: this.bodyParams.agents } : {};
const department = Livechat.saveDepartment(null, this.bodyParams.department, agents);
const agents = this.bodyParams.agents ? { upsert: this.bodyParams.agents } : {};
const department = Livechat.saveDepartment(null, this.bodyParams.department, agents);

if (department) {
return API.v1.success({
department,
agents: LivechatDepartmentAgents.find({ departmentId: department._id }).fetch(),
});
}

return API.v1.failure();
} catch (e) {
return API.v1.failure(e);
if (department) {
return API.v1.success({
department,
agents: LivechatDepartmentAgents.find({ departmentId: department._id }).fetch(),
});
}

return API.v1.failure();
},
},
);
Expand Down Expand Up @@ -170,20 +164,22 @@ API.v1.addRoute(
'livechat/department.autocomplete',
{ authRequired: true },
{
get() {
async get() {
if (!hasAtLeastOnePermission(this.userId, ['view-livechat-departments', 'view-l-room'])) {
return API.v1.unauthorized();
}

const { selector, onlyMyDepartments } = this.queryParams;
if (!selector) {
return API.v1.failure("The 'selector' param is required");
}

return API.v1.success(
Promise.await(
findDepartmentsToAutocomplete({
uid: this.userId,
selector: JSON.parse(selector),
onlyMyDepartments: onlyMyDepartments === 'true',
}),
),
await findDepartmentsToAutocomplete({
uid: this.userId,
selector: JSON.parse(selector),
onlyMyDepartments: onlyMyDepartments === 'true',
}),
);
},
},
Expand Down Expand Up @@ -239,7 +235,11 @@ API.v1.addRoute(
'livechat/department.listByIds',
{ authRequired: true },
{
get() {
async get() {
if (!hasAtLeastOnePermission(this.userId, ['view-livechat-departments', 'view-l-room'])) {
return API.v1.unauthorized();
}

const { ids } = this.queryParams;
const { fields } = this.parseJsonQuery();
if (!ids) {
Expand All @@ -250,13 +250,10 @@ API.v1.addRoute(
}

return API.v1.success(
Promise.await(
findDepartmentsBetweenIds({
uid: this.userId,
ids,
fields,
}),
),
await findDepartmentsBetweenIds({
ids,
fields,
}),
);
},
},
Expand Down
9 changes: 0 additions & 9 deletions apps/meteor/app/livechat/server/api/lib/departments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,9 +110,6 @@ export async function findDepartmentsToAutocomplete({
selector,
onlyMyDepartments = false,
}: FindDepartmentToAutocompleteParams): Promise<{ items: ILivechatDepartmentRecord[] }> {
if (!(await hasPermissionAsync(uid, 'view-livechat-departments')) && !(await hasPermissionAsync(uid, 'view-l-room'))) {
return { items: [] };
}
const { exceptions = [] } = selector;
let { conditions = {} } = selector;

Expand Down Expand Up @@ -160,18 +157,12 @@ export async function findDepartmentAgents({
}

export async function findDepartmentsBetweenIds({
uid,
ids,
fields,
}: {
uid: string;
ids: string[];
fields: Record<string, unknown>;
}): Promise<{ departments: ILivechatDepartmentRecord[] }> {
if (!(await hasPermissionAsync(uid, 'view-livechat-departments')) && !(await hasPermissionAsync(uid, 'view-l-room'))) {
throw new Error('error-not-authorized');
}

const departments = await LivechatDepartment.findInIds(ids, fields).toArray();
return { departments };
}
5 changes: 3 additions & 2 deletions apps/meteor/app/livechat/server/lib/Helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -600,14 +600,15 @@ export const updateDepartmentAgents = (departmentId, agents, departmentEnabled)
}

upsert.forEach((agent) => {
if (!Users.findOneById(agent.agentId, { fields: { _id: 1 } })) {
const agentFromDb = Users.findOneById(agent.agentId, { fields: { _id: 1, username: 1 } });
if (!agentFromDb) {
return;
}

LivechatDepartmentAgents.saveAgent({
agentId: agent.agentId,
departmentId,
username: agent.username,
username: agentFromDb.username,
count: agent.count ? parseInt(agent.count) : 0,
order: agent.order ? parseInt(agent.order) : 0,
departmentEnabled,
Expand Down
4 changes: 4 additions & 0 deletions apps/meteor/app/livechat/server/lib/Livechat.js
Original file line number Diff line number Diff line change
Expand Up @@ -1079,6 +1079,10 @@ export const Livechat = {
);
}

if (fallbackForwardDepartment && !LivechatDepartment.findOneById(fallbackForwardDepartment)) {
throw new Meteor.Error('error-fallback-department-not-found', 'Fallback department not found', { method: 'livechat:saveDepartment' });
}

const departmentDB = LivechatDepartment.createOrUpdateDepartment(_id, departmentData);
if (departmentDB && departmentAgents) {
updateDepartmentAgents(departmentDB._id, departmentAgents, departmentDB.enabled);
Expand Down
15 changes: 15 additions & 0 deletions apps/meteor/tests/data/livechat/rooms.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,21 @@ export const createVisitor = () =>
});
});

export const createDepartment = () => {
return new Promise((resolve, reject) => {
request
.post(api('livechat/department'))
.set(credentials)
.send({ department: { name: `Department ${Date.now()}`, enabled: true, showOnOfflineForm: true, showOnRegistration: true, email: 'a@b.com' } })
.end((err, res) => {
if (err) {
return reject(err);
}
resolve(res.body.department);
});
});
}

export const createAgent = () =>
new Promise((resolve, reject) => {
request
Expand Down
2 changes: 2 additions & 0 deletions apps/meteor/tests/end-to-end/api/livechat/00-rooms.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
/* eslint-env mocha */

import { expect } from 'chai';
import { IOmnichannelRoom, IVisitor } from '@rocket.chat/core-typings';
import { Response } from 'supertest';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
/* eslint-env mocha */

import { expect } from 'chai';
import type { ILivechatVisitor, IOmnichannelRoom } from '@rocket.chat/core-typings';
import { Response } from 'supertest';
Expand Down
Loading