[IMPROVE] Omnichannel Visitor Endpoints error handling (#23819)

Co-authored-by: Murtaza Patrawala <34130764+murtaza98@users.noreply.github.com>
pull/23901/head
Kevin Aleman 4 years ago committed by GitHub
parent 8290e0ce17
commit 4455e88321
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 11
      app/api/server/api.d.ts
  2. 2
      app/livechat/server/api/rest.js
  3. 177
      app/livechat/server/api/v1/visitor.js
  4. 179
      app/livechat/server/api/v1/visitor.ts
  5. 18
      app/models/server/raw/LivechatVisitors.ts
  6. 18
      definition/ILivechatVisitor.ts
  7. 25
      definition/rest/v1/omnichannel.ts

@ -41,6 +41,14 @@ type UnauthorizedResult<T> = {
};
}
type NotFoundResult<T> = {
statusCode: 403;
body: {
success: false;
error: T | 'Resource not found';
};
}
export type NonEnterpriseTwoFactorOptions = {
authRequired: true;
forceTwoFactorAuthenticationForNonEnterprise: true;
@ -72,6 +80,7 @@ type ActionThis<TMethod extends Method, TPathPattern extends PathPattern, TOptio
readonly queryParams: TMethod extends 'GET' ? Partial<OperationParams<TMethod, TPathPattern>> : Record<string, string>;
// TODO make it unsafe
readonly bodyParams: TMethod extends 'GET' ? Record<string, unknown> : Partial<OperationParams<TMethod, TPathPattern>>;
readonly request: Request;
requestParams(): OperationParams<TMethod, TPathPattern>;
getPaginationItems(): {
readonly offset: number;
@ -165,6 +174,8 @@ declare class APIClass<TBasePath extends string = '/'> {
unauthorized<T>(msg?: T): UnauthorizedResult<T>;
notFound<T>(msg?: T): NotFoundResult<T>;
defaultFieldsToExclude: {
joinCode: 0;
members: 0;

@ -1,5 +1,5 @@
import './v1/config.js';
import './v1/visitor.js';
import './v1/visitor';
import './v1/transcript.js';
import './v1/offlineMessage.js';
import './v1/pageVisited.js';

@ -1,177 +0,0 @@
import { Meteor } from 'meteor/meteor';
import { Match, check } from 'meteor/check';
import { LivechatRooms, LivechatVisitors, LivechatCustomField } from '../../../../models';
import { hasPermission } from '../../../../authorization';
import { API } from '../../../../api/server';
import { findGuest, normalizeHttpHeaderData } from '../lib/livechat';
import { Livechat } from '../../lib/Livechat';
API.v1.addRoute('livechat/visitor', {
post() {
try {
check(this.bodyParams, {
visitor: Match.ObjectIncluding({
token: String,
name: Match.Maybe(String),
email: Match.Maybe(String),
department: Match.Maybe(String),
phone: Match.Maybe(String),
username: Match.Maybe(String),
customFields: Match.Maybe([
Match.ObjectIncluding({
key: String,
value: String,
overwrite: Boolean,
}),
]),
}),
});
const { token, customFields } = this.bodyParams.visitor;
const guest = this.bodyParams.visitor;
if (this.bodyParams.visitor.phone) {
guest.phone = { number: this.bodyParams.visitor.phone };
}
guest.connectionData = normalizeHttpHeaderData(this.request.headers);
const visitorId = Livechat.registerGuest(guest);
let visitor = LivechatVisitors.getVisitorByToken(token);
// If it's updating an existing visitor, it must also update the roomInfo
const cursor = LivechatRooms.findOpenByVisitorToken(token);
cursor.forEach((room) => {
Livechat.saveRoomInfo(room, visitor);
});
if (customFields && customFields instanceof Array) {
customFields.forEach((field) => {
const customField = LivechatCustomField.findOneById(field.key);
if (!customField) {
return;
}
const { key, value, overwrite } = field;
if (customField.scope === 'visitor' && !LivechatVisitors.updateLivechatDataByToken(token, key, value, overwrite)) {
return API.v1.failure();
}
});
}
visitor = LivechatVisitors.findOneById(visitorId);
return API.v1.success({ visitor });
} catch (e) {
return API.v1.failure(e);
}
},
});
API.v1.addRoute('livechat/visitor/:token', {
get() {
try {
check(this.urlParams, {
token: String,
});
const visitor = LivechatVisitors.getVisitorByToken(this.urlParams.token);
return API.v1.success({ visitor });
} catch (e) {
return API.v1.failure(e.error);
}
},
delete() {
try {
check(this.urlParams, {
token: String,
});
const visitor = LivechatVisitors.getVisitorByToken(this.urlParams.token);
if (!visitor) {
throw new Meteor.Error('invalid-token');
}
const { _id } = visitor;
const result = Livechat.removeGuest(_id);
if (result) {
return API.v1.success({
visitor: {
_id,
ts: new Date().toISOString(),
},
});
}
return API.v1.failure();
} catch (e) {
return API.v1.failure(e.error);
}
},
});
API.v1.addRoute('livechat/visitor/:token/room', { authRequired: true }, {
get() {
if (!hasPermission(this.userId, 'view-livechat-manager')) {
return API.v1.unauthorized();
}
const rooms = LivechatRooms.findOpenByVisitorToken(this.urlParams.token, {
fields: {
name: 1,
t: 1,
cl: 1,
u: 1,
usernames: 1,
servedBy: 1,
},
}).fetch();
return API.v1.success({ rooms });
},
});
API.v1.addRoute('livechat/visitor.callStatus', {
post() {
try {
check(this.bodyParams, {
token: String,
callStatus: String,
rid: String,
callId: String,
});
const { token, callStatus, rid, callId } = this.bodyParams;
const guest = findGuest(token);
if (!guest) {
throw new Meteor.Error('invalid-token');
}
const status = callStatus;
Livechat.updateCallStatus(callId, rid, status, guest);
return API.v1.success({ token, callStatus });
} catch (e) {
return API.v1.failure(e);
}
},
});
API.v1.addRoute('livechat/visitor.status', {
post() {
try {
check(this.bodyParams, {
token: String,
status: String,
});
const { token, status } = this.bodyParams;
const guest = findGuest(token);
if (!guest) {
throw new Meteor.Error('invalid-token');
}
Livechat.notifyGuestStatusChanged(token, status);
return API.v1.success({ token, status });
} catch (e) {
return API.v1.failure(e);
}
},
});

@ -0,0 +1,179 @@
import { Meteor } from 'meteor/meteor';
import { Match, check } from 'meteor/check';
import { LivechatRooms, LivechatVisitors, LivechatCustomField } from '../../../../models/server';
import { LivechatVisitors as VisitorsRaw } from '../../../../models/server/raw';
import { API } from '../../../../api/server';
import { findGuest, normalizeHttpHeaderData } from '../lib/livechat';
import { Livechat } from '../../lib/Livechat';
import { ILivechatVisitorDTO } from '../../../../../definition/ILivechatVisitor';
import { IRoom } from '../../../../../definition/IRoom';
API.v1.addRoute('livechat/visitor', {
async post() {
check(this.bodyParams, {
visitor: Match.ObjectIncluding({
token: String,
name: Match.Maybe(String),
email: Match.Maybe(String),
department: Match.Maybe(String),
phone: Match.Maybe(String),
username: Match.Maybe(String),
customFields: Match.Maybe([
Match.ObjectIncluding({
key: String,
value: String,
overwrite: Boolean,
}),
]),
}),
});
const { token, customFields } = this.bodyParams.visitor;
const guest: ILivechatVisitorDTO = { ...this.bodyParams.visitor };
if (this.bodyParams.visitor.phone) {
guest.phone = { number: this.bodyParams.visitor.phone as string };
}
guest.connectionData = normalizeHttpHeaderData(this.request.headers);
const visitorId = Livechat.registerGuest(guest);
let visitor = await VisitorsRaw.getVisitorByToken(token, {});
// If it's updating an existing visitor, it must also update the roomInfo
const cursor = LivechatRooms.findOpenByVisitorToken(token);
cursor.forEach((room: IRoom) => {
Livechat.saveRoomInfo(room, visitor);
});
if (customFields && customFields instanceof Array) {
customFields.forEach((field) => {
const customField = LivechatCustomField.findOneById(field.key);
if (!customField) {
return;
}
const { key, value, overwrite } = field;
if (customField.scope === 'visitor' && !LivechatVisitors.updateLivechatDataByToken(token, key, value, overwrite)) {
return API.v1.failure();
}
});
visitor = await VisitorsRaw.findOneById(visitorId, {});
}
if (!visitor) {
throw new Meteor.Error('error-saving-visitor', 'An error ocurred while saving visitor');
}
return API.v1.success({ visitor });
},
});
API.v1.addRoute('livechat/visitor/:token', {
async get() {
check(this.urlParams, {
token: String,
});
const visitor = await VisitorsRaw.getVisitorByToken(this.urlParams.token, {});
if (!visitor) {
throw new Meteor.Error('invalid-token');
}
return API.v1.success({ visitor });
},
async delete() {
check(this.urlParams, {
token: String,
});
const visitor = await VisitorsRaw.getVisitorByToken(this.urlParams.token, {});
if (!visitor) {
throw new Meteor.Error('invalid-token');
}
const rooms = LivechatRooms.findOpenByVisitorToken(this.urlParams.token, {
fields: {
name: 1,
t: 1,
cl: 1,
u: 1,
usernames: 1,
servedBy: 1,
},
}).fetch();
if (rooms && rooms.length) {
throw new Meteor.Error('visitor-has-open-rooms', 'Cannot remove visitors with opened rooms');
}
const { _id } = visitor;
const result = Livechat.removeGuest(_id);
if (!result) {
throw new Meteor.Error('error-removing-visitor', 'An error ocurred while deleting visitor');
}
return API.v1.success({
visitor: {
_id,
ts: new Date().toISOString(),
},
});
},
});
API.v1.addRoute('livechat/visitor/:token/room', { authRequired: true, permissionsRequired: ['view-livechat-manager'] }, {
async get() {
const rooms = LivechatRooms.findOpenByVisitorToken(this.urlParams.token, {
fields: {
name: 1,
t: 1,
cl: 1,
u: 1,
usernames: 1,
servedBy: 1,
},
}).fetch();
return API.v1.success({ rooms });
},
});
API.v1.addRoute('livechat/visitor.callStatus', {
async post() {
check(this.bodyParams, {
token: String,
callStatus: String,
rid: String,
callId: String,
});
const { token, callStatus, rid, callId } = this.bodyParams;
const guest = findGuest(token);
if (!guest) {
throw new Meteor.Error('invalid-token');
}
Livechat.updateCallStatus(callId, rid, callStatus, guest);
return API.v1.success({ token, callStatus });
},
});
API.v1.addRoute('livechat/visitor.status', {
async post() {
check(this.bodyParams, {
token: String,
status: String,
});
const { token, status } = this.bodyParams;
const guest = findGuest(token);
if (!guest) {
throw new Meteor.Error('invalid-token');
}
Livechat.notifyGuestStatusChanged(token, status);
return API.v1.success({ token, status });
},
});

@ -1,10 +1,26 @@
import { escapeRegExp } from '@rocket.chat/string-helpers';
import { AggregationCursor, Cursor, FilterQuery, FindOneOptions } from 'mongodb';
import { AggregationCursor, Cursor, FilterQuery, FindOneOptions, WithoutProjection } from 'mongodb';
import { BaseRaw } from './BaseRaw';
import { ILivechatVisitor } from '../../../../definition/ILivechatVisitor';
export class LivechatVisitorsRaw extends BaseRaw<ILivechatVisitor> {
findOneById(_id: string, options: WithoutProjection<FindOneOptions<ILivechatVisitor>>): Promise<ILivechatVisitor | null> {
const query = {
_id,
};
return this.findOne(query, options);
}
getVisitorByToken(token: string, options: WithoutProjection<FindOneOptions<ILivechatVisitor>>): Promise<ILivechatVisitor | null> {
const query = {
token,
};
return this.findOne(query, options);
}
getVisitorsBetweenDate({ start, end, department }: { start: Date; end: Date; department: string }): Cursor<ILivechatVisitor> {
const query = {
_updatedAt: {

@ -33,3 +33,21 @@ export interface ILivechatVisitor extends IRocketChatRecord {
host?: string;
visitorEmails?: IVisitorEmail[];
}
export interface ILivechatVisitorDTO {
id: string;
token: string;
name: string;
email: string;
department: string;
phone: string | { number: string };
username: string;
customFields: {
key: string;
value: string;
overwrite: boolean;
}[];
connectionData: {
httpHeaders: Record<string, string>;
};
}

@ -1,6 +1,7 @@
import { ILivechatDepartment } from '../../ILivechatDepartment';
import { ILivechatMonitor } from '../../ILivechatMonitor';
import { ILivechatTag } from '../../ILivechatTag';
import { ILivechatVisitor, ILivechatVisitorDTO } from '../../ILivechatVisitor';
import { IMessage } from '../../IMessage';
import { IOmnichannelRoom, IRoom } from '../../IRoom';
import { ISetting } from '../../ISetting';
@ -116,4 +117,28 @@ export type OmnichannelEndpoints = {
}[];
}>;
};
'livechat/visitor': {
POST: (params: { visitor: ILivechatVisitorDTO }) => { visitor: ILivechatVisitor };
};
'livechat/visitor/:token': {
GET: (params: { token: string }) => { visitor: ILivechatVisitor };
DELETE: (params: { token: string }) => { visitor: { _id: string; ts: string } };
};
'livechat/visitor/:token/room': {
GET: (params: { token: string }) => { rooms: IOmnichannelRoom[] };
};
'livechat/visitor.callStatus': {
POST: (params: { token: string; callStatus: string; rid: string; callId: string }) => {
token: string;
callStatus: string;
};
};
'livechat/visitor.status': {
POST: (params: { token: string; status: string }) => { token: string; status: string };
};
};

Loading…
Cancel
Save