Compare commits
35 Commits
51f68d5021
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 0cce4f8085 | |||
| 816e0a1381 | |||
| 4973a80f64 | |||
| 7d6d218e08 | |||
| ca8d50241e | |||
| 5dd83863cb | |||
| c51ed1ba8f | |||
| be1495d8b3 | |||
| b314b361b2 | |||
| 2a4a1e8e14 | |||
| 0e64f654ec | |||
| 037b001de6 | |||
| ae9f0fae73 | |||
| 58f176b344 | |||
| 6c1b3f8b20 | |||
| 3f788733c8 | |||
| ca011abee5 | |||
| 83769bb816 | |||
| 4d4d649458 | |||
| 3da24d067b | |||
| b43fed0fec | |||
| 9bbb23de48 | |||
| a114b1aaaf | |||
| 6fa75f531a | |||
| f0d4d29181 | |||
| f56fae304e | |||
| 8b0969792a | |||
| b48a7cd4f2 | |||
| 74e2fcee52 | |||
| c1be8ed22e | |||
| ebbbb6ad57 | |||
| fa15cce2e1 | |||
| a141e18120 | |||
| 7df9f1e481 | |||
| 4650a4d5a3 |
@@ -1,2 +1,3 @@
|
|||||||
node_modules
|
node_modules
|
||||||
npm-debug.log
|
npm-debug.log
|
||||||
|
.*
|
||||||
|
|||||||
14
.env
14
.env
@@ -1,7 +1,7 @@
|
|||||||
EO_API_USERNAME = apiclient
|
PORT = 3000
|
||||||
EO_API_PASSWORD = apiclient12345
|
|
||||||
EO_API_SCOPE = oidc tags context_entitlements content_entitlements em_api_access
|
## Store type 'local-storage' or 'redis' supported
|
||||||
EO_API_CLIENT_ID = default
|
STORE_PROVIDER = redis
|
||||||
EO_API_CLIENT_SECRET =
|
|
||||||
EO_API_ACCESS_TOKEN_URL = https://em5.verint.training/oidc-token-service/default/token
|
# for example if STORE = redis
|
||||||
EO_API_UDG_URL = https://em5.verint.training/unified-data-gateway/default/graphql
|
STORE_URL = redis://localhost:6379
|
||||||
22
.eslintrc.cjs
Normal file
22
.eslintrc.cjs
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
module.exports = {
|
||||||
|
env: {
|
||||||
|
node: true,
|
||||||
|
},
|
||||||
|
extends: ["eslint:recommended", "prettier"],
|
||||||
|
overrides: [
|
||||||
|
{
|
||||||
|
env: {
|
||||||
|
node: true,
|
||||||
|
},
|
||||||
|
files: [".eslintrc.{js,cjs}"],
|
||||||
|
parserOptions: {
|
||||||
|
sourceType: "module",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
parserOptions: {
|
||||||
|
ecmaVersion: "latest",
|
||||||
|
sourceType: "module",
|
||||||
|
},
|
||||||
|
rules: {},
|
||||||
|
};
|
||||||
1869
.gitignore
vendored
1869
.gitignore
vendored
File diff suppressed because it is too large
Load Diff
1
.prettierrc.json
Normal file
1
.prettierrc.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{}
|
||||||
@@ -12,7 +12,7 @@ WORKDIR /usr/src/app
|
|||||||
COPY package*.json ./
|
COPY package*.json ./
|
||||||
|
|
||||||
# continuous integration for production
|
# continuous integration for production
|
||||||
RUN npm ci --only=production
|
RUN npm ci --omit=dev
|
||||||
|
|
||||||
# Bundle app source
|
# Bundle app source
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|||||||
29
README.md
29
README.md
@@ -1,10 +1,29 @@
|
|||||||
# eo-services
|
# eo-services
|
||||||
|
|
||||||
created from template docker-image-template
|
This is the API backend for the eo-services client
|
||||||
|
|
||||||
|
## Authentication
|
||||||
|
|
||||||
|
Using Desktop rules call \<hostname>/api/auth using an Adaptives Framework's URL 'POST' with Application Sercurity turned on and the following application/json request body:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"type": "authentication",
|
||||||
|
"id": "1",
|
||||||
|
"attributes": {
|
||||||
|
"host": "em5.verint.training"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> Note: you will need to add the cacert for the API host to your Application Server environment otherwise you will receive a "Remote Exception thrown while invoking external URLhttps://\<yourhost>:443/api/auth Exception:sun.security.validator.ValidatorException: PKIX "
|
||||||
|
>
|
||||||
|
> Exception:sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
|
||||||
|
|
||||||
## Key Files
|
## Key Files
|
||||||
|
|
||||||
* Jenkinsfile: Automation of build and deploy of docker image.
|
- Jenkinsfile: Automation of build and deploy of docker image.
|
||||||
* Dockerfile: Docker Image specification.
|
- Dockerfile: Docker Image specification.
|
||||||
* docker-bake.hcl: Docker build defintion file.
|
- docker-bake.hcl: Docker build defintion file.
|
||||||
|
|
||||||
|
|||||||
1992
examples/example_udg_response.json
Normal file
1992
examples/example_udg_response.json
Normal file
File diff suppressed because it is too large
Load Diff
1159
node_modules/.package-lock.json
generated
vendored
1159
node_modules/.package-lock.json
generated
vendored
File diff suppressed because it is too large
Load Diff
16
nodemon.json
Normal file
16
nodemon.json
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"restartable": "rs",
|
||||||
|
"watch": ["src", ".env"],
|
||||||
|
"ext": "js,ts,json",
|
||||||
|
"execMap": {
|
||||||
|
"js": "node --harmony"
|
||||||
|
},
|
||||||
|
"ignore": [".git", "node_modules/**/node_modules"],
|
||||||
|
"events": {
|
||||||
|
"restart": "osascript -e 'display notification \"App restarted due to:\n'$FILENAME'\" with title \"nodemon\"'"
|
||||||
|
},
|
||||||
|
"verbose": true,
|
||||||
|
"env": {
|
||||||
|
"NODE_ENV": "development"
|
||||||
|
}
|
||||||
|
}
|
||||||
5089
package-lock.json
generated
5089
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
34
package.json
34
package.json
@@ -5,8 +5,10 @@
|
|||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "echo \"Error: no test specified\" && exit 1",
|
"test": "echo \"Error: no test specified\" && exit 1",
|
||||||
"start": "node index.js",
|
"lint": "eslint --ext .js,.cjs --ignore-path .gitignore --fix src/",
|
||||||
"dev": "nodemon index.js",
|
"format": "prettier . --write",
|
||||||
|
"dev": "nodemon src/app.js",
|
||||||
|
"start": "NODE_ENV=production node src/app.js",
|
||||||
"build": "docker buildx bake",
|
"build": "docker buildx bake",
|
||||||
"push": "docker buildx bake --push"
|
"push": "docker buildx bake --push"
|
||||||
},
|
},
|
||||||
@@ -15,12 +17,34 @@
|
|||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.1.2",
|
"axios": "^1.1.2",
|
||||||
|
"base64url": "^3.0.1",
|
||||||
|
"compression": "^1.7.4",
|
||||||
|
"cookie-parser": "^1.4.6",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"dotenv": "^16.0.3",
|
"dotenv": "^16.0.3",
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"express-session": "^1.17.3",
|
"express-session": "^1.17.3",
|
||||||
|
"helmet": "^7.0.0",
|
||||||
|
"http-errors": "^2.0.0",
|
||||||
|
"jsonld": "^8.3.1",
|
||||||
|
"jsonwebtoken": "^9.0.1",
|
||||||
|
"jwk-to-pem": "^2.0.5",
|
||||||
|
"local-storage": "^2.0.0",
|
||||||
"morgan": "^1.10.0",
|
"morgan": "^1.10.0",
|
||||||
"nodemon": "^2.0.20",
|
"nodemon": "^3.0.1",
|
||||||
"query-string": "^7.1.1"
|
"promise": "^8.3.0",
|
||||||
}
|
"query-string": "^7.1.1",
|
||||||
|
"redis": "^4.6.7",
|
||||||
|
"winston": "^3.9.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"eslint": "^8.43.0",
|
||||||
|
"eslint-config-prettier": "^8.8.0",
|
||||||
|
"eslint-config-standard": "^17.1.0",
|
||||||
|
"eslint-plugin-import": "^2.26.0",
|
||||||
|
"eslint-plugin-n": "^16.0.1",
|
||||||
|
"eslint-plugin-promise": "^6.1.1",
|
||||||
|
"prettier": "2.8.8"
|
||||||
|
},
|
||||||
|
"type": "module"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,240 +0,0 @@
|
|||||||
const express = require("express");
|
|
||||||
const router = express.Router();
|
|
||||||
const axios = require("axios");
|
|
||||||
const url = require("url");
|
|
||||||
const util = require("util");
|
|
||||||
|
|
||||||
router.get("/", (req, res) => {
|
|
||||||
// token in session -> get user data and send it back to the vue app
|
|
||||||
if (req.session.token) {
|
|
||||||
query(req.query.referenceId);
|
|
||||||
}
|
|
||||||
// no token -> send nothing
|
|
||||||
else {
|
|
||||||
const params = new url.URLSearchParams({
|
|
||||||
grant_type: "password",
|
|
||||||
username: process.env.EO_API_USERNAME,
|
|
||||||
password: process.env.EO_API_PASSWORD,
|
|
||||||
scope: process.env.EO_API_SCOPE,
|
|
||||||
client_id: process.env.EO_API_CLIENT_ID,
|
|
||||||
client_secret: process.env.EO_API_SECRET,
|
|
||||||
});
|
|
||||||
|
|
||||||
axios
|
|
||||||
.post(process.env.EO_API_ACCESS_TOKEN_URL, params.toString(), {
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/x-www-form-urlencoded",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.then((result) => {
|
|
||||||
// save token to session
|
|
||||||
req.session.token = result.data.access_token;
|
|
||||||
console.log(result);
|
|
||||||
|
|
||||||
query(req.query.referenceId);
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
sendError(err, res);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function query(referenceId) {
|
|
||||||
console.log("Executing Query");
|
|
||||||
|
|
||||||
var query = `query ($startTime: DateTime, $endTime: DateTime) {
|
|
||||||
findContactsCompletedBetween(startTime: $startTime, endTime: $endTime, filter: {interactionTypes : EMAIL}) {
|
|
||||||
|
|
||||||
totalCount
|
|
||||||
edges {
|
|
||||||
node {
|
|
||||||
systemId
|
|
||||||
startTime
|
|
||||||
endTime
|
|
||||||
direction
|
|
||||||
handledBy {
|
|
||||||
username
|
|
||||||
firstName
|
|
||||||
lastName
|
|
||||||
nickname
|
|
||||||
orgScope
|
|
||||||
}
|
|
||||||
activeDuration
|
|
||||||
notes {
|
|
||||||
totalCount
|
|
||||||
edges {
|
|
||||||
node {
|
|
||||||
text
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
interaction {
|
|
||||||
systemId
|
|
||||||
locale
|
|
||||||
__typename
|
|
||||||
... on Email {
|
|
||||||
messageId
|
|
||||||
threadId
|
|
||||||
sentDate
|
|
||||||
receivedDate
|
|
||||||
subject
|
|
||||||
fromAddress
|
|
||||||
toAddresses
|
|
||||||
ccAddresses
|
|
||||||
bccAddresses
|
|
||||||
detectedLanguage
|
|
||||||
mailboxName
|
|
||||||
attachmentCount
|
|
||||||
isDuplicate
|
|
||||||
}
|
|
||||||
}
|
|
||||||
outcome {
|
|
||||||
totalCount
|
|
||||||
edges {
|
|
||||||
node {
|
|
||||||
text
|
|
||||||
isActive
|
|
||||||
isVisible
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
customer {
|
|
||||||
totalCount
|
|
||||||
edges {
|
|
||||||
node {
|
|
||||||
ref
|
|
||||||
firstName
|
|
||||||
lastName
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
queue {
|
|
||||||
name
|
|
||||||
orgScope
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}`;
|
|
||||||
var startTime = new Date(
|
|
||||||
new Date().setFullYear(new Date().getFullYear() - 1)
|
|
||||||
);
|
|
||||||
var endTime = new Date(new Date().setHours(new Date().getHours() - 1));
|
|
||||||
|
|
||||||
axios
|
|
||||||
.post(
|
|
||||||
process.env.EO_API_UDG_URL,
|
|
||||||
JSON.stringify({
|
|
||||||
query,
|
|
||||||
variables: { startTime, endTime },
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
Authorization: `OIDC_id_token ${req.session.token}`,
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.then((result) => {
|
|
||||||
var contacts = result.data.data.findContactsCompletedBetween.edges;
|
|
||||||
var filteredContacts = [];
|
|
||||||
contacts.forEach(function (contact, i) {
|
|
||||||
if (contact.node.interaction.__typename === "Email") {
|
|
||||||
// threadId if Reference in Subject line will do
|
|
||||||
if (
|
|
||||||
(contact.node.interaction.threadId === referenceId) |
|
|
||||||
contact.node.interaction.subject.includes(
|
|
||||||
`<< Ref:${referenceId} >>`
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
filteredContacts.push(contact);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
result.data.data.findContactsCompletedBetween.edges = filteredContacts;
|
|
||||||
result.data.data.findContactsCompletedBetween.totalCount =
|
|
||||||
filteredContacts.length;
|
|
||||||
|
|
||||||
// Summary Values
|
|
||||||
var summary = {};
|
|
||||||
|
|
||||||
summary.totalCount = filteredContacts.length;
|
|
||||||
summary.totalInboundCount = 0;
|
|
||||||
summary.totalInboundActiveSeconds = 0;
|
|
||||||
|
|
||||||
summary.firstContactReceivedDate = new Date(
|
|
||||||
filteredContacts[0].node.interaction.receivedDate
|
|
||||||
);
|
|
||||||
|
|
||||||
filteredContacts.forEach(function (contact, i) {
|
|
||||||
if (contact.node.direction === "INBOUND") {
|
|
||||||
summary.totalInboundCount++;
|
|
||||||
if (!summary.firstInboundContactStartDate) {
|
|
||||||
summary.firstInboundContactStartDate = new Date(
|
|
||||||
contact.node.startTime
|
|
||||||
);
|
|
||||||
|
|
||||||
summary.firstContactReceivedDate = new Date(
|
|
||||||
contact.node.interaction.receivedDate
|
|
||||||
);
|
|
||||||
}
|
|
||||||
summary.totalInboundActiveSeconds += contact.node.activeDuration;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// TODO: Because of overlapping contacts, we may need to calculate max instead of last.
|
|
||||||
summary.lastContactEndTime = new Date(
|
|
||||||
filteredContacts[filteredContacts.length - 1].node.endTime
|
|
||||||
);
|
|
||||||
|
|
||||||
summary.totalHTHours =
|
|
||||||
(summary.lastContactEndTime.getTime() -
|
|
||||||
summary.firstContactReceivedDate.getTime()) /
|
|
||||||
(1000 * 3600);
|
|
||||||
summary.activeHTMinutes =
|
|
||||||
(summary.lastContactEndTime.getTime() -
|
|
||||||
summary.firstInboundContactStartDate.getTime()) /
|
|
||||||
(1000 * 60);
|
|
||||||
|
|
||||||
result.data.data.summary = summary;
|
|
||||||
|
|
||||||
if (result.data) {
|
|
||||||
console.log(
|
|
||||||
util.inspect(summary, {
|
|
||||||
showHidden: false,
|
|
||||||
depth: null,
|
|
||||||
colors: true,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
res.send(result.data);
|
|
||||||
}
|
|
||||||
|
|
||||||
// expired token -> send nothing
|
|
||||||
else {
|
|
||||||
req.session.destroy();
|
|
||||||
res.send({});
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
// bin the token on error
|
|
||||||
req.session.destroy();
|
|
||||||
sendError(err, res);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
module.exports = router;
|
|
||||||
function sendError(err, res) {
|
|
||||||
console.error(err);
|
|
||||||
let errStatus = 500;
|
|
||||||
if (err.response) errStatus = err.response.status;
|
|
||||||
res.status(errStatus).send({
|
|
||||||
errors: [
|
|
||||||
{
|
|
||||||
status: errStatus,
|
|
||||||
title: err.code,
|
|
||||||
detail: err.message,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
16
src/api/middlewares/authKey.js
Normal file
16
src/api/middlewares/authKey.js
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { store } from "../../utils/index.js";
|
||||||
|
import createHttpError from "http-errors";
|
||||||
|
|
||||||
|
export default (req, res, next) => {
|
||||||
|
store.get(req.query.authKey).then((value) => {
|
||||||
|
if (value == null) {
|
||||||
|
next(
|
||||||
|
new createHttpError.Forbidden(
|
||||||
|
`middleware:authKey ${req.query.authKey} not authenticated`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
next();
|
||||||
|
};
|
||||||
12
src/api/middlewares/detailedRequestLogging.js
Normal file
12
src/api/middlewares/detailedRequestLogging.js
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { logger } from "../../utils/index.js";
|
||||||
|
|
||||||
|
export default (req, res, next) => {
|
||||||
|
logger.debug(
|
||||||
|
`URI: ${req.method} ${req.protocol}://${req.hostname}:${req.client.localPort}${req.originalUrl}`
|
||||||
|
);
|
||||||
|
logger.debug(`Headers: ${JSON.stringify(req.headers, null, 2)}`);
|
||||||
|
logger.debug(`Cookies: ${JSON.stringify(req.cookies, null, 2)}`);
|
||||||
|
logger.debug(`Query: ${JSON.stringify(req.query, null, 2)}`);
|
||||||
|
logger.debug(`Body: ${JSON.stringify(req.body, null, 2)}`);
|
||||||
|
next();
|
||||||
|
};
|
||||||
9
src/api/middlewares/index.js
Normal file
9
src/api/middlewares/index.js
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
// import { Router } from "express";
|
||||||
|
import authKey from "./authKey.js";
|
||||||
|
import detailedRequestLogging from "./detailedRequestLogging.js";
|
||||||
|
|
||||||
|
export default (router) => {
|
||||||
|
router.use("/", detailedRequestLogging);
|
||||||
|
router.use("/interactions-flow", authKey);
|
||||||
|
router.use("/unified-data-gateway", authKey);
|
||||||
|
};
|
||||||
112
src/api/routes/auth.js
Normal file
112
src/api/routes/auth.js
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
import { Router } from "express";
|
||||||
|
import axios from "axios";
|
||||||
|
import { logger, store } from "../../utils/index.js";
|
||||||
|
import { decode } from "jsonwebtoken";
|
||||||
|
import jwt from "jsonwebtoken";
|
||||||
|
import jwkToPem from "jwk-to-pem";
|
||||||
|
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
|
router.all("/", (req, res, next) => {
|
||||||
|
var token = null;
|
||||||
|
var sessionIdentifier = null;
|
||||||
|
const cookiePrefix = "__Host-VRNTOTCT";
|
||||||
|
|
||||||
|
if (req.cookies) {
|
||||||
|
logger.debug(
|
||||||
|
`Checking ${cookiePrefix}... Cookie as this takes presidence over Authorization Header ...`
|
||||||
|
);
|
||||||
|
Object.keys(req.cookies).forEach((key) => {
|
||||||
|
if (key.startsWith(cookiePrefix)) {
|
||||||
|
logger.info(`Found ${key} cookie using value for token`);
|
||||||
|
token = req.cookies[key];
|
||||||
|
sessionIdentifier = key.substring(cookiePrefix.length);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (!token && req.headers.authorization) {
|
||||||
|
logger.debug("Checking Authorization Header for OIDC_id_token ...");
|
||||||
|
var authHeader = req.headers.authorization;
|
||||||
|
const prefix = "OIDC_id_token ";
|
||||||
|
if (authHeader && authHeader.startsWith(prefix)) {
|
||||||
|
logger.info("Found OIDC_id_token in Authorization Header");
|
||||||
|
token = authHeader.substring(prefix.length, authHeader.length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (token) {
|
||||||
|
const decoded = decode(token);
|
||||||
|
logger.debug(`Decoded [${JSON.stringify(decoded, null, 2)}]`);
|
||||||
|
|
||||||
|
if (decoded.iss) {
|
||||||
|
const jwkURI = decoded.iss + "/connect/jwk_uri";
|
||||||
|
|
||||||
|
logger.debug(`Requesting JWK on ${jwkURI}`);
|
||||||
|
|
||||||
|
axios
|
||||||
|
.get(jwkURI)
|
||||||
|
.then((jwkResponse) => {
|
||||||
|
logger.debug(
|
||||||
|
`Response JWK on ${JSON.stringify(jwkResponse.data, null, 2)}`
|
||||||
|
);
|
||||||
|
|
||||||
|
if (
|
||||||
|
jwkResponse.data &&
|
||||||
|
jwkResponse.data.keys &&
|
||||||
|
jwkResponse.data.keys.length > 0
|
||||||
|
) {
|
||||||
|
const key = jwkResponse.data.keys[0];
|
||||||
|
var pem = jwkToPem(key);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const verified = jwt.verify(token, pem, {
|
||||||
|
algorithms: [key.alg],
|
||||||
|
});
|
||||||
|
logger.debug(`Verified [${JSON.stringify(verified, null, 2)}]`);
|
||||||
|
|
||||||
|
const issSplit = verified.iss.split("/oidc-token-service/");
|
||||||
|
|
||||||
|
const authData = {
|
||||||
|
token: token,
|
||||||
|
host: issSplit[0],
|
||||||
|
tenant: issSplit[1],
|
||||||
|
};
|
||||||
|
|
||||||
|
const authDataString = JSON.stringify(authData);
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
`Adding agent ${verified.sub} to store for environment ${authDataString}}`
|
||||||
|
);
|
||||||
|
store
|
||||||
|
.set(verified.sub, authData)
|
||||||
|
.then(() => {
|
||||||
|
if (sessionIdentifier && sessionIdentifier.length > 0) {
|
||||||
|
logger.info(
|
||||||
|
`Adding sessionIdentifier ${sessionIdentifier} to store for environment ${authDataString}`
|
||||||
|
);
|
||||||
|
return store.set(sessionIdentifier, authData);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
res.send(decoded);
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
logger.error(e); // "Uh-oh!"
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
logger.error(`Verify failed [${JSON.stringify(err, null, 2)}].`);
|
||||||
|
next(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
logger.error(`JWK Request failed [${JSON.stringify(err, null, 2)}].`);
|
||||||
|
next(err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
next(new Error("Unable to authenticate, no token found"));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default router;
|
||||||
12
src/api/routes/config.js
Normal file
12
src/api/routes/config.js
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { Router } from "express";
|
||||||
|
import config from "../../config/index.js";
|
||||||
|
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
|
router.get("/", (req, res) => {
|
||||||
|
res.send({
|
||||||
|
config: config,
|
||||||
|
env: process.env,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
export default router;
|
||||||
19
src/api/routes/index.js
Normal file
19
src/api/routes/index.js
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { Router } from "express";
|
||||||
|
import config from "./config.js";
|
||||||
|
import auth from "./auth.js";
|
||||||
|
import interactionsFlows from "./interactions-flow.js";
|
||||||
|
import udg from "./unified-data-gateway.js";
|
||||||
|
import tps from "./tps.js";
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
|
import middlewares from "../middlewares/index.js";
|
||||||
|
|
||||||
|
middlewares(router);
|
||||||
|
|
||||||
|
router.use("/config", config);
|
||||||
|
router.use("/auth", auth);
|
||||||
|
router.use("/interactions-flow", interactionsFlows);
|
||||||
|
router.use("/unified-data-gateway", udg);
|
||||||
|
router.use("/tps", tps);
|
||||||
|
|
||||||
|
export default router;
|
||||||
174
src/api/routes/interactions-flow.js
Normal file
174
src/api/routes/interactions-flow.js
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
import { Router } from "express";
|
||||||
|
import axios from "axios";
|
||||||
|
const router = Router();
|
||||||
|
import { logger, store } from "../../utils/index.js";
|
||||||
|
import { referenceIdQuery } from "../../utils/graphQueries.js";
|
||||||
|
|
||||||
|
router.get("/", async (req, res) => {
|
||||||
|
const data = {
|
||||||
|
nodes: [],
|
||||||
|
links: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const filter = req.query.filter;
|
||||||
|
|
||||||
|
var chain = {
|
||||||
|
"Direction-Channel": true,
|
||||||
|
"Channel-SubChannel": true,
|
||||||
|
"Channel-Queue": true,
|
||||||
|
"SubChannel-Queue": true,
|
||||||
|
"Queue-Outcome": true,
|
||||||
|
"Channel-Outcome": true, // special case for outbound that does not go throught queue
|
||||||
|
};
|
||||||
|
// TODO: Chain will be hardcoded until I can work out filter logic
|
||||||
|
// var filterNames = filter.split(",");
|
||||||
|
// for (let index = 0; index < filterNames.length - 1; index++) {
|
||||||
|
// chain[filterNames[index] + "-" + filterNames[index + 1]] = true;
|
||||||
|
// }
|
||||||
|
|
||||||
|
const startTime = new Date(
|
||||||
|
new Date().setFullYear(new Date().getFullYear() - 2)
|
||||||
|
);
|
||||||
|
var endTime = new Date();
|
||||||
|
// FIX for 'endTime' parameter cannot be later than one minute prior to now.
|
||||||
|
endTime.setMinutes(endTime.getMinutes() - 1);
|
||||||
|
|
||||||
|
const auth = await store.get(req.query.authKey);
|
||||||
|
const { host, tenant, token } = auth;
|
||||||
|
axios
|
||||||
|
.post(
|
||||||
|
`${host}/unified-data-gateway/${tenant}/graphql`,
|
||||||
|
JSON.stringify({
|
||||||
|
query: referenceIdQuery,
|
||||||
|
variables: { startTime, endTime },
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `OIDC_id_token ${token}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.then((result) => {
|
||||||
|
if (result.data.errors && result.data.errors.length > 0) {
|
||||||
|
result.data.errors.forEach(function (error) {
|
||||||
|
logger.error("ERROR: Errors in results - " + error.message);
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO: Should keep errors for filteredContacts
|
||||||
|
result.data.errors = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!result.data.data.findContactsCompletedBetween) {
|
||||||
|
logger.debug("No findContactsCompletedBetween returned.");
|
||||||
|
res.send(data);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
result.data.data.findContactsCompletedBetween.edges.forEach((value) => {
|
||||||
|
// TODO: Should add INBOUND and OUTBOUND Nodes before Channel
|
||||||
|
addNode(filter, "Direction", value.node.direction, data);
|
||||||
|
addNode(filter, "Channel", value.node.interaction.__typename, data);
|
||||||
|
if (chain["Direction-Channel"]) {
|
||||||
|
addLink(
|
||||||
|
data,
|
||||||
|
value.node.direction,
|
||||||
|
value.node.interaction.__typename
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value.node.interaction.subChannel) {
|
||||||
|
addNode(
|
||||||
|
filter,
|
||||||
|
"SubChannel",
|
||||||
|
value.node.interaction.subChannel,
|
||||||
|
data
|
||||||
|
);
|
||||||
|
if (chain["Channel-SubChannel"]) {
|
||||||
|
addLink(
|
||||||
|
data,
|
||||||
|
value.node.interaction.__typename,
|
||||||
|
value.node.interaction.subChannel
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value.node.queue) {
|
||||||
|
const queueName = `${value.node.queue.name}(${value.node.queue.orgScope})`;
|
||||||
|
addNode(filter, "Queue", queueName, data);
|
||||||
|
if (
|
||||||
|
chain["SubChannel-Queue"] &&
|
||||||
|
!!value.node.interaction.subChannel
|
||||||
|
) {
|
||||||
|
addLink(data, value.node.interaction.subChannel, queueName);
|
||||||
|
} else if (chain["Channel-Queue"]) {
|
||||||
|
addLink(data, value.node.interaction.__typename, queueName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value.node.outcome) {
|
||||||
|
value.node.outcome.edges.forEach((element) => {
|
||||||
|
addNode(filter, "Outcome", element.node.text, data);
|
||||||
|
|
||||||
|
if (chain["Queue-Outcome"] && !!value.node.queue) {
|
||||||
|
const queueName = `${value.node.queue.name}(${value.node.queue.orgScope})`;
|
||||||
|
addLink(
|
||||||
|
data,
|
||||||
|
queueName,
|
||||||
|
element.node.text,
|
||||||
|
1 / value.node.outcome.edges.length
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (chain["Channel-Outcome"] && !value.node.queue) {
|
||||||
|
addLink(
|
||||||
|
data,
|
||||||
|
value.node.interaction.__typename,
|
||||||
|
element.node.text,
|
||||||
|
1 / value.node.outcome.edges.length
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.debug(`Sending data: ${JSON.stringify(data, null, 2)}`);
|
||||||
|
res.send(data);
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
logger.error(e); // "Uh-oh!"
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function addLink(data, source, target, _fraction) {
|
||||||
|
var fraction = _fraction ? _fraction : 1;
|
||||||
|
var index = data.links.findIndex((i) => {
|
||||||
|
return i.source == source && i.target == target;
|
||||||
|
});
|
||||||
|
if (index != -1) {
|
||||||
|
data.links[index].value++;
|
||||||
|
} else {
|
||||||
|
data.links.push({
|
||||||
|
source: source,
|
||||||
|
target: target,
|
||||||
|
value: fraction,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function addNode(filter, category, name, data) {
|
||||||
|
if (
|
||||||
|
filter.split(",").indexOf(category) != -1 &&
|
||||||
|
!!name &&
|
||||||
|
data.nodes.findIndex(
|
||||||
|
(value) => value.name === name && value.category === category
|
||||||
|
) == -1
|
||||||
|
) {
|
||||||
|
data.nodes.push({
|
||||||
|
name: name,
|
||||||
|
category: category,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default router;
|
||||||
208
src/api/routes/tps.js
Normal file
208
src/api/routes/tps.js
Normal file
@@ -0,0 +1,208 @@
|
|||||||
|
import { Router } from "express";
|
||||||
|
import axios from "axios";
|
||||||
|
import { logger, store, isEmpty } from "../../utils/index.js";
|
||||||
|
import createHttpError from "http-errors";
|
||||||
|
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
|
router.get("/", async (req, res, next) => {
|
||||||
|
const auth = await store.get(req.query.authKey);
|
||||||
|
|
||||||
|
if (isEmpty(auth)) {
|
||||||
|
next(
|
||||||
|
new createHttpError.Forbidden(
|
||||||
|
`No authenication information found in store`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { host, tenant, token } = auth;
|
||||||
|
logger.debug(`tps GET all properties from ${host}`);
|
||||||
|
axios
|
||||||
|
.get(
|
||||||
|
`${host}/tenant-properties-service/${tenant}/properties?fields=name,value,lastModifiedDate,lastModifiedBy`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `OIDC_id_token ${token}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.then((result) => {
|
||||||
|
if (result.data.errors && result.data.errors.length > 0) {
|
||||||
|
result.data.errors.forEach(function (error) {
|
||||||
|
logger.error("ERROR: Errors in results - " + error.message);
|
||||||
|
});
|
||||||
|
next(new Error("Error(s) getting properties "));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const totalItems = result.data["hydra:totalItems"];
|
||||||
|
logger.debug("tps result has hydra:totalItems [" + totalItems + "]");
|
||||||
|
if (totalItems > 0) {
|
||||||
|
const data = result.data["hydra:member"].map(function (member) {
|
||||||
|
return {
|
||||||
|
name: member["vcfg:name"],
|
||||||
|
value: member["vcfg:value"],
|
||||||
|
lastModifiedDate: member["vcfg:lastModifiedDate"],
|
||||||
|
lastModifiedBy: member["vcfg:lastModifiedBy"],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
res.json({ data: data });
|
||||||
|
} else {
|
||||||
|
res.json({ date: [] });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get("/:name", async (req, res, next) => {
|
||||||
|
const auth = await store.get(req.query.authKey);
|
||||||
|
|
||||||
|
if (isEmpty(auth)) {
|
||||||
|
next(
|
||||||
|
new createHttpError.Forbidden(
|
||||||
|
`No authenication information found in store`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const propertyName = req.params.name;
|
||||||
|
const { host, tenant, token } = auth;
|
||||||
|
logger.debug(`tps GET ${propertyName} from ${host}`);
|
||||||
|
|
||||||
|
if (!propertyName || propertyName.length == 0) {
|
||||||
|
next(new createHttpError.NotFound(`Blank property keys not allowed`));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
axios
|
||||||
|
.get(
|
||||||
|
`${host}/tenant-properties-service/${tenant}/properties?q=${propertyName}`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `OIDC_id_token ${token}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.then((result) => {
|
||||||
|
if (result.data.errors && result.data.errors.length > 0) {
|
||||||
|
result.data.errors.forEach(function (error) {
|
||||||
|
logger.error("ERROR: Errors in results - " + error.message);
|
||||||
|
});
|
||||||
|
next(new Error("Error(s) getting propertyName " + propertyName));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const totalItems = result.data["hydra:totalItems"];
|
||||||
|
logger.debug("tps result has hydra:totalItems [" + totalItems + "]");
|
||||||
|
if (totalItems > 0) {
|
||||||
|
const member = result.data["hydra:member"][0];
|
||||||
|
res.json({
|
||||||
|
data: { name: member["vcfg:name"], value: member["vcfg:value"] },
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
next(
|
||||||
|
new createHttpError.NotFound(`property ${propertyName} not found`)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post("/", async (req, res, next) => {
|
||||||
|
const auth = await store.get(req.query.authKey);
|
||||||
|
|
||||||
|
if (isEmpty(auth)) {
|
||||||
|
next(
|
||||||
|
new createHttpError.Forbidden(
|
||||||
|
`No authenication information found in store`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { host, tenant, token } = auth;
|
||||||
|
const property = req.body.data;
|
||||||
|
const body = `[
|
||||||
|
{
|
||||||
|
"@type" : "vcfg:PropertyUpdateOrCreate",
|
||||||
|
"vcfg:name" :"${property.name}",
|
||||||
|
"vcfg:value" : ${JSON.stringify(property.value)}
|
||||||
|
}
|
||||||
|
]`;
|
||||||
|
|
||||||
|
logger.debug(`tps PATCH ${body} to ${host}`);
|
||||||
|
|
||||||
|
axios
|
||||||
|
.patch(`${host}/tenant-properties-service/${tenant}/properties`, body, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `OIDC_id_token ${token}`,
|
||||||
|
"Content-Type": "application/ld+json",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.catch(function (error) {
|
||||||
|
if (error.response) {
|
||||||
|
// The request was made and the server responded with a status code
|
||||||
|
// that falls out of the range of 2xx
|
||||||
|
logger.error(JSON.stringify(error.response.data));
|
||||||
|
logger.error(JSON.stringify(error.response.status));
|
||||||
|
logger.error(JSON.stringify(error.response.headers));
|
||||||
|
} else if (error.request) {
|
||||||
|
// The request was made but no response was received
|
||||||
|
// `error.request` is an instance of XMLHttpRequest in the browser
|
||||||
|
// and an instance of http.ClientRequest in node.js
|
||||||
|
logger.error(JSON.stringify(error.request));
|
||||||
|
} else {
|
||||||
|
// Something happened in setting up the request that triggered an Error
|
||||||
|
logger.error("Error", error.message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
router.delete("/:name", async (req, res, next) => {
|
||||||
|
const auth = await store.get(req.query.authKey);
|
||||||
|
|
||||||
|
if (isEmpty(auth)) {
|
||||||
|
next(
|
||||||
|
new createHttpError.Forbidden(
|
||||||
|
`No authenication information found in store`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { host, tenant, token } = auth;
|
||||||
|
const propertyName = req.params.name;
|
||||||
|
const body = `[
|
||||||
|
{
|
||||||
|
"@type" : "vcfg:PropertyDelete",
|
||||||
|
"vcfg:name" : "${propertyName}"
|
||||||
|
}
|
||||||
|
]`;
|
||||||
|
|
||||||
|
logger.debug(`tps PATCH ${body} to ${host}`);
|
||||||
|
|
||||||
|
axios
|
||||||
|
.patch(`${host}/tenant-properties-service/${tenant}/properties`, body, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `OIDC_id_token ${token}`,
|
||||||
|
"Content-Type": "application/ld+json",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.catch(function (error) {
|
||||||
|
if (error.response) {
|
||||||
|
// The request was made and the server responded with a status code
|
||||||
|
// that falls out of the range of 2xx
|
||||||
|
logger.error(JSON.stringify(error.response.data));
|
||||||
|
logger.error(JSON.stringify(error.response.status));
|
||||||
|
logger.error(JSON.stringify(error.response.headers));
|
||||||
|
} else if (error.request) {
|
||||||
|
// The request was made but no response was received
|
||||||
|
// `error.request` is an instance of XMLHttpRequest in the browser
|
||||||
|
// and an instance of http.ClientRequest in node.js
|
||||||
|
logger.error(JSON.stringify(error.request));
|
||||||
|
} else {
|
||||||
|
// Something happened in setting up the request that triggered an Error
|
||||||
|
logger.error("Error", error.message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
export default router;
|
||||||
139
src/api/routes/unified-data-gateway.js
Normal file
139
src/api/routes/unified-data-gateway.js
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
import { Router } from "express";
|
||||||
|
import axios from "axios";
|
||||||
|
import { logger } from "../../utils/index.js";
|
||||||
|
// import { URLSearchParams } from "url";
|
||||||
|
import { inspect } from "util";
|
||||||
|
import localStorage from "local-storage";
|
||||||
|
import { referenceIdQuery } from "../../utils/graphQueries.js";
|
||||||
|
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
|
router.get("/", (req, res, next) => {
|
||||||
|
query(req.query.referenceId);
|
||||||
|
|
||||||
|
function query(referenceId) {
|
||||||
|
logger.info(`Executing Query with ${referenceId}`);
|
||||||
|
|
||||||
|
const startTime = new Date(
|
||||||
|
new Date().setFullYear(new Date().getFullYear() - 2)
|
||||||
|
);
|
||||||
|
var endTime = new Date();
|
||||||
|
// FIX for 'endTime' parameter cannot be later than one minute prior to now.
|
||||||
|
endTime.setMinutes(endTime.getMinutes() - 1);
|
||||||
|
|
||||||
|
const { host, tenant, token } = localStorage(req.query.authKey);
|
||||||
|
axios
|
||||||
|
.post(
|
||||||
|
`${host}/unified-data-gateway/${tenant}/graphql`,
|
||||||
|
JSON.stringify({
|
||||||
|
query: referenceIdQuery,
|
||||||
|
variables: { startTime, endTime },
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `OIDC_id_token ${token}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.then((result) => {
|
||||||
|
if (result.data.errors && result.data.errors.length > 0) {
|
||||||
|
result.data.errors.forEach(function (error) {
|
||||||
|
logger.error("ERROR: Errors in results - " + error.message);
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO: Should keep errors for filteredContacts
|
||||||
|
result.data.errors = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const contacts = result.data.data.findContactsCompletedBetween.edges;
|
||||||
|
|
||||||
|
const filteredContacts = [];
|
||||||
|
contacts.forEach(function (contact) {
|
||||||
|
if (contact.node.interaction.__typename === "Email") {
|
||||||
|
// threadId if Reference in Subject line will do
|
||||||
|
if (
|
||||||
|
(contact.node.interaction.threadId === referenceId) |
|
||||||
|
contact.node.interaction.subject.includes(
|
||||||
|
`<< Ref:${referenceId} >>`
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
filteredContacts.push(contact);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
result.data.data.findContactsCompletedBetween.edges = filteredContacts;
|
||||||
|
result.data.data.findContactsCompletedBetween.totalCount =
|
||||||
|
filteredContacts.length;
|
||||||
|
|
||||||
|
// Summary Values
|
||||||
|
const summary = {};
|
||||||
|
|
||||||
|
summary.totalCount = filteredContacts.length;
|
||||||
|
summary.totalInboundCount = 0;
|
||||||
|
summary.totalInboundActiveSeconds = 0;
|
||||||
|
|
||||||
|
if (summary.totalCount > 0) {
|
||||||
|
summary.firstContactReceivedDate = new Date(
|
||||||
|
filteredContacts[0].node.interaction.receivedDate
|
||||||
|
);
|
||||||
|
|
||||||
|
filteredContacts.forEach(function (contact) {
|
||||||
|
if (contact.node.direction === "INBOUND") {
|
||||||
|
summary.totalInboundCount++;
|
||||||
|
if (!summary.firstInboundContactStartDate) {
|
||||||
|
summary.firstInboundContactStartDate = new Date(
|
||||||
|
contact.node.startTime
|
||||||
|
);
|
||||||
|
|
||||||
|
summary.firstContactReceivedDate = new Date(
|
||||||
|
contact.node.interaction.receivedDate
|
||||||
|
);
|
||||||
|
}
|
||||||
|
summary.totalInboundActiveSeconds += contact.node.activeDuration;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO: Because of overlapping contacts, we may need to calculate max instead of last.
|
||||||
|
summary.lastContactEndTime = new Date(
|
||||||
|
filteredContacts[filteredContacts.length - 1].node.endTime
|
||||||
|
);
|
||||||
|
|
||||||
|
summary.totalHTHours =
|
||||||
|
(summary.lastContactEndTime.getTime() -
|
||||||
|
summary.firstContactReceivedDate.getTime()) /
|
||||||
|
(1000 * 3600);
|
||||||
|
summary.activeHTMinutes =
|
||||||
|
(summary.lastContactEndTime.getTime() -
|
||||||
|
summary.firstInboundContactStartDate.getTime()) /
|
||||||
|
(1000 * 60);
|
||||||
|
}
|
||||||
|
result.data.data.summary = summary;
|
||||||
|
|
||||||
|
if (result.data) {
|
||||||
|
logger.debug(
|
||||||
|
inspect(summary, {
|
||||||
|
showHidden: false,
|
||||||
|
depth: null,
|
||||||
|
colors: true,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
res.send(result.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
// expired token -> send nothing
|
||||||
|
else {
|
||||||
|
req.session.destroy();
|
||||||
|
res.send({});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
// bin the token on error
|
||||||
|
req.session.destroy();
|
||||||
|
next(err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
export default router;
|
||||||
18
src/app.js
Normal file
18
src/app.js
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import express from "express";
|
||||||
|
import config from "./config/index.js";
|
||||||
|
import loader from "./loaders/index.js";
|
||||||
|
import { logger } from "./utils/index.js";
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
|
||||||
|
loader(app);
|
||||||
|
|
||||||
|
app.listen(config.port, (err) => {
|
||||||
|
if (err) {
|
||||||
|
logger.info(err);
|
||||||
|
return process.exit(1);
|
||||||
|
}
|
||||||
|
logger.info(`Server is running on ${config.port}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default app;
|
||||||
39
src/config/index.js
Normal file
39
src/config/index.js
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import dotenv from "dotenv";
|
||||||
|
|
||||||
|
// Set the NODE_ENV to 'development' by default
|
||||||
|
process.env.NODE_ENV = process.env.NODE_ENV || "development";
|
||||||
|
|
||||||
|
if (process.env.NODE_ENV !== "production") {
|
||||||
|
const envFound = dotenv.config({ debug: true });
|
||||||
|
if (envFound.error) {
|
||||||
|
// This error should crash whole process
|
||||||
|
|
||||||
|
throw new Error("⚠️ Couldn't find .env file ⚠️");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
/**
|
||||||
|
* Your favorite port
|
||||||
|
*/
|
||||||
|
port: parseInt(process.env.PORT || 3000, 10),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used by winston logger
|
||||||
|
*/
|
||||||
|
logs: {
|
||||||
|
level: process.env.LOG_LEVEL || "silly",
|
||||||
|
},
|
||||||
|
|
||||||
|
store: {
|
||||||
|
provider: process.env.STORE_PROVIDER || "local-storage",
|
||||||
|
url: process.env.STORE_URL || "redis://localhost:6379",
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API configs
|
||||||
|
*/
|
||||||
|
api: {
|
||||||
|
prefix: process.env.API_PREFIX || "/api",
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -1,9 +1,7 @@
|
|||||||
const express = require("express");
|
const express = require("express");
|
||||||
const cors = require("cors");
|
const cors = require("cors");
|
||||||
const morgan = require("morgan");
|
|
||||||
const session = require("express-session");
|
const session = require("express-session");
|
||||||
const axios = require("axios");
|
import { logger } from "./utils/index.js";
|
||||||
const qs = require("query-string");
|
|
||||||
|
|
||||||
// dotenv
|
// dotenv
|
||||||
require("dotenv").config();
|
require("dotenv").config();
|
||||||
@@ -12,7 +10,6 @@ const app = express();
|
|||||||
|
|
||||||
// Use our middlewares
|
// Use our middlewares
|
||||||
app.use(cors({ origin: true, credentials: true }));
|
app.use(cors({ origin: true, credentials: true }));
|
||||||
app.use(morgan("common"));
|
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
app.use(
|
app.use(
|
||||||
session({
|
session({
|
||||||
@@ -32,7 +29,7 @@ const port = process.env.SERVER_PORT || 3000;
|
|||||||
|
|
||||||
// Listen to server
|
// Listen to server
|
||||||
app.listen(port, () => {
|
app.listen(port, () => {
|
||||||
console.log(`Listening on port ${port}`);
|
logger.info(`Listening on port ${port}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
// ...
|
// ...
|
||||||
@@ -44,4 +41,6 @@ app.get("/", (req, res) => {
|
|||||||
});
|
});
|
||||||
// ...
|
// ...
|
||||||
|
|
||||||
|
app.use("/config", require("./routes/config"));
|
||||||
app.use("/unified-data-gateway", require("./routes/unified-data-gateway"));
|
app.use("/unified-data-gateway", require("./routes/unified-data-gateway"));
|
||||||
|
app.use("/interactions-flow", require("./routes/interactions-flow"));
|
||||||
103
src/loaders/express.js
Normal file
103
src/loaders/express.js
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
import express from "express";
|
||||||
|
import cors from "cors";
|
||||||
|
import compression from "compression";
|
||||||
|
import morgan from "morgan";
|
||||||
|
import helmet from "helmet";
|
||||||
|
import config from "./../config/index.js";
|
||||||
|
import routes from "./../api/routes/index.js";
|
||||||
|
import { logger } from "../utils/index.js";
|
||||||
|
// import { rateLimiter } from '../api/middlewares/index.js';
|
||||||
|
import bodyParser from "body-parser";
|
||||||
|
import session from "express-session";
|
||||||
|
import cookieParser from "cookie-parser";
|
||||||
|
|
||||||
|
export default (app) => {
|
||||||
|
logger.info("Loading Express ...");
|
||||||
|
process.on("uncaughtException", async (error) => {
|
||||||
|
logger.error(error);
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on("unhandledRejection", async (ex) => {
|
||||||
|
logger.error(ex);
|
||||||
|
});
|
||||||
|
|
||||||
|
app.enable("trust proxy");
|
||||||
|
// app.use(cors());
|
||||||
|
|
||||||
|
app.use(cors({ origin: true, credentials: true }));
|
||||||
|
app.use(bodyParser.urlencoded({ extended: false }));
|
||||||
|
app.use(bodyParser.json());
|
||||||
|
// app.use(morgan("dev"));
|
||||||
|
app.use(
|
||||||
|
morgan("common", {
|
||||||
|
stream: {
|
||||||
|
write: (message) => logger.info(message.trim()),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
app.use(helmet());
|
||||||
|
app.use(compression());
|
||||||
|
app.use(cookieParser());
|
||||||
|
app.use(express.static("public"));
|
||||||
|
app.disable("x-powered-by");
|
||||||
|
app.disable("etag");
|
||||||
|
app.use(
|
||||||
|
session({
|
||||||
|
secret: "1234567890", // don't use this secret in prod :)
|
||||||
|
resave: false,
|
||||||
|
saveUninitialized: false,
|
||||||
|
cookie: {
|
||||||
|
secure: "auto",
|
||||||
|
httpOnly: true,
|
||||||
|
maxAge: 3600000,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// app.use(rateLimiter);
|
||||||
|
logger.info(`Mounting routes on ${config.api.prefix}`);
|
||||||
|
app.use(config.api.prefix, routes);
|
||||||
|
|
||||||
|
app.get("/", (_req, res) => {
|
||||||
|
return res
|
||||||
|
.status(200)
|
||||||
|
.json({
|
||||||
|
resultMessage: {
|
||||||
|
en: "Project is successfully working...",
|
||||||
|
},
|
||||||
|
resultCode: "00004",
|
||||||
|
})
|
||||||
|
.end();
|
||||||
|
});
|
||||||
|
|
||||||
|
app.use((req, res, next) => {
|
||||||
|
// res.header("Access-Control-Allow-Origin", "*");
|
||||||
|
// res.header(
|
||||||
|
// "Access-Control-Allow-Headers",
|
||||||
|
// "Origin, X-Requested-With, Content-Type, Accept, Authorization"
|
||||||
|
// );
|
||||||
|
// res.header("Content-Security-Policy-Report-Only", "default-src: https:");
|
||||||
|
// if (req.method === "OPTIONS") {
|
||||||
|
// res.header("Access-Control-Allow-Methods", "PUT POST PATCH DELETE GET");
|
||||||
|
// return res.status(200).json({});
|
||||||
|
// }
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
|
||||||
|
app.use((_req, _res, next) => {
|
||||||
|
const error = new Error("Endpoint could not find!");
|
||||||
|
error.status = 404;
|
||||||
|
next(error);
|
||||||
|
});
|
||||||
|
|
||||||
|
app.use((error, req, res) => {
|
||||||
|
res.status(error.status || 500);
|
||||||
|
logger.error(error.message);
|
||||||
|
return res.json({
|
||||||
|
resultMessage: {
|
||||||
|
en: error.message,
|
||||||
|
tr: error.message,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
5
src/loaders/index.js
Normal file
5
src/loaders/index.js
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import expressLoader from "./express.js";
|
||||||
|
|
||||||
|
export default async (app) => {
|
||||||
|
expressLoader(app);
|
||||||
|
};
|
||||||
74
src/utils/graphQueries.js
Normal file
74
src/utils/graphQueries.js
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
export const referenceIdQuery = `query ($startTime: DateTime, $endTime: DateTime) {
|
||||||
|
findContactsCompletedBetween(startTime: $startTime, endTime: $endTime) {
|
||||||
|
|
||||||
|
totalCount
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
systemId
|
||||||
|
startTime
|
||||||
|
endTime
|
||||||
|
direction
|
||||||
|
handledBy {
|
||||||
|
username
|
||||||
|
firstName
|
||||||
|
lastName
|
||||||
|
nickname
|
||||||
|
orgScope
|
||||||
|
}
|
||||||
|
activeDuration
|
||||||
|
notes {
|
||||||
|
totalCount
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
interaction {
|
||||||
|
systemId
|
||||||
|
locale
|
||||||
|
__typename
|
||||||
|
... on Email {
|
||||||
|
messageId
|
||||||
|
threadId
|
||||||
|
sentDate
|
||||||
|
receivedDate
|
||||||
|
subject
|
||||||
|
fromAddress
|
||||||
|
toAddresses
|
||||||
|
ccAddresses
|
||||||
|
bccAddresses
|
||||||
|
detectedLanguage
|
||||||
|
mailboxName
|
||||||
|
attachmentCount
|
||||||
|
isDuplicate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
outcome {
|
||||||
|
totalCount
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
text
|
||||||
|
isActive
|
||||||
|
isVisible
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
customer {
|
||||||
|
totalCount
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
ref
|
||||||
|
firstName
|
||||||
|
lastName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
queue {
|
||||||
|
name
|
||||||
|
orgScope
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`;
|
||||||
11
src/utils/index.js
Normal file
11
src/utils/index.js
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
export { default as logger } from "./logger.js";
|
||||||
|
export { default as store } from "./store.js";
|
||||||
|
export function isEmpty(obj) {
|
||||||
|
for (const prop in obj) {
|
||||||
|
if (Object.hasOwn(obj, prop)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
32
src/utils/logger.js
Normal file
32
src/utils/logger.js
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import winston from "winston";
|
||||||
|
import config from "../config/index.js";
|
||||||
|
|
||||||
|
const transports = [];
|
||||||
|
if (process.env.NODE_ENV !== "development") {
|
||||||
|
transports.push(new winston.transports.Console());
|
||||||
|
} else {
|
||||||
|
transports.push(
|
||||||
|
new winston.transports.Console({
|
||||||
|
format: winston.format.combine(
|
||||||
|
winston.format.cli(),
|
||||||
|
winston.format.splat()
|
||||||
|
),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const LoggerInstance = winston.createLogger({
|
||||||
|
level: config.logs.level,
|
||||||
|
levels: winston.config.npm.levels,
|
||||||
|
format: winston.format.combine(
|
||||||
|
winston.format.timestamp({
|
||||||
|
format: "YYYY-MM-DD HH:mm:ss",
|
||||||
|
}),
|
||||||
|
winston.format.errors({ stack: true }),
|
||||||
|
winston.format.splat(),
|
||||||
|
winston.format.json()
|
||||||
|
),
|
||||||
|
transports,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default LoggerInstance;
|
||||||
21142
src/utils/sampleUDGresponse.js
Normal file
21142
src/utils/sampleUDGresponse.js
Normal file
File diff suppressed because one or more lines are too long
59
src/utils/store.js
Normal file
59
src/utils/store.js
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import { createClient } from "redis";
|
||||||
|
import localStorage from "local-storage";
|
||||||
|
import config from "../config/index.js";
|
||||||
|
import { logger } from "./index.js";
|
||||||
|
import Promise from "promise";
|
||||||
|
|
||||||
|
const store = await createStore();
|
||||||
|
|
||||||
|
async function createStore() {
|
||||||
|
if (config.store.provider === "redis") {
|
||||||
|
return {
|
||||||
|
client: await createRedisClient(),
|
||||||
|
|
||||||
|
set: async function (key, value) {
|
||||||
|
return this.client.hSet(key, value);
|
||||||
|
},
|
||||||
|
get: async function (key) {
|
||||||
|
return this.client.hGetAll(key);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
set: function (key, value) {
|
||||||
|
return new Promise(function (resolve, reject) {
|
||||||
|
if (localStorage(key, value)) resolve(true);
|
||||||
|
else reject(Error("unable to set using local-storage"));
|
||||||
|
});
|
||||||
|
},
|
||||||
|
get: function (key) {
|
||||||
|
return new Promise(function (resolve) {
|
||||||
|
resolve(localStorage(key));
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createRedisClient() {
|
||||||
|
const client = createClient({
|
||||||
|
url: config.store.url,
|
||||||
|
});
|
||||||
|
|
||||||
|
client.on("error", (err) => {
|
||||||
|
if (err.errors) {
|
||||||
|
err.errors.forEach((error) => {
|
||||||
|
logger.error(`Redis Client Error ${error}`);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
logger.error(`Redis Client Error ${err.message}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await client.connect();
|
||||||
|
logger.info(`Connect store to ${config.store.url}`);
|
||||||
|
|
||||||
|
return client;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default store;
|
||||||
Reference in New Issue
Block a user