diff --git a/README.md b/README.md index f45f01f..f8c7358 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,44 @@ # sdp-request-watcher -A locally run JS app to monitor for request closures using Zoho's SDP Cloud API. -## Reference Materials +## Env and Server Setup +env.example contains the setup for the env requires a Non-Browser Application key from https://api-console.zoho.com/add +Place the .env file inside the src directory + +Technicians and Statuses are checked in the .env file + +I resolved the issue with it only finding closed tickets so it should now find resolved and closed I havent tested with others but will + +The technician field can have spaces after the comma however the statuses can not. There can be no spaces before the status or it wont work properly. + + + +## Site Setup +Open the site after the server is running, then connect the Zoho account from the board. + +1. Start the server from the `src` folder with node ./server.js +2. Open the local dashboard in your browser. Should be localhost:3000 +3. Click **Connect** to begin the Zoho authorization flow. +4. Approve the device code on the Zoho page, then return to the dashboard. + +What you should see: + +![Site setup step 1](images/1.png) + +![Site setup step 2](images/2.png) + +![Site setup step 3](images/3.png) + +![Site setup step 4](images/4.png) + +Once connected, the board will begin checking for tickets automatically and refreshing on its own based on the time set in the server.js file. + +## Requirements +axios +dotenv + + +## Reference Materials/ Provided by Skylar - [Non-Browser Apps - Overview](https://www.zoho.com/accounts/protocol/oauth/limited-input-devices.html) - [Non-Browser Apps - Initiation Request](https://www.zoho.com/accounts/protocol/oauth/devices/initiation-request.html) diff --git a/images/1.png b/images/1.png new file mode 100644 index 0000000..43de6be Binary files /dev/null and b/images/1.png differ diff --git a/images/2.png b/images/2.png new file mode 100644 index 0000000..9d6d016 Binary files /dev/null and b/images/2.png differ diff --git a/images/3.png b/images/3.png new file mode 100644 index 0000000..e76fdc5 Binary files /dev/null and b/images/3.png differ diff --git a/images/4.png b/images/4.png new file mode 100644 index 0000000..0e214a1 Binary files /dev/null and b/images/4.png differ diff --git a/package-lock.json b/package-lock.json index 0bcfd87..e5e7068 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,51 @@ { - "name": "sdp-request-watcher", + "name": "Dashboard", "lockfileVersion": 3, "requires": true, "packages": { "": { "dependencies": { "axios": "^1.13.2", - "dotenv": "^17.2.3" + "dotenv": "^17.4.2", + "express": "^5.2.1" + } + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/accepts/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/accepts/node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/asynckit": { @@ -26,6 +65,52 @@ "proxy-from-env": "^1.1.0" } }, + "node_modules/body-parser": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz", + "integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^2.0.0", + "debug": "^4.4.3", + "http-errors": "^2.0.1", + "iconv-lite": "^0.7.2", + "on-finished": "^2.4.1", + "qs": "^6.15.2", + "raw-body": "^3.0.2", + "type-is": "^2.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/body-parser/node_modules/content-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", + "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/call-bind-apply-helpers": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", @@ -39,6 +124,22 @@ "node": ">= 0.4" } }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -51,6 +152,63 @@ "node": ">= 0.8" } }, + "node_modules/content-disposition": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", + "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -60,10 +218,19 @@ "node": ">=0.4.0" } }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/dotenv": { - "version": "17.2.3", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz", - "integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==", + "version": "17.4.2", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz", + "integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==", "license": "BSD-2-Clause", "engines": { "node": ">=12" @@ -86,6 +253,21 @@ "node": ">= 0.4" } }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", @@ -131,6 +313,110 @@ "node": ">= 0.4" } }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express/node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/follow-redirects": { "version": "1.15.11", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", @@ -167,6 +453,24 @@ "node": ">= 6" } }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -264,6 +568,63 @@ "node": ">= 0.4" } }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz", + "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -273,6 +634,31 @@ "node": ">= 0.4" } }, + "node_modules/media-typer": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.1.tgz", + "integrity": "sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", @@ -294,11 +680,403 @@ "node": ">= 0.6" } }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", "license": "MIT" + }, + "node_modules/qs": { + "version": "6.15.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz", + "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==", + "license": "BSD-3-Clause", + "dependencies": { + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.3.0.tgz", + "integrity": "sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/send/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/send/node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/type-is": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", + "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", + "license": "MIT", + "dependencies": { + "content-type": "^2.0.0", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/type-is/node_modules/content-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", + "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/type-is/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/type-is/node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" } } } diff --git a/package.json b/package.json index 15f3cd4..f79061a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "dependencies": { "axios": "^1.13.2", - "dotenv": "^17.2.3" + "dotenv": "^17.4.2", + "express": "^5.2.1" } } diff --git a/src/auth.js b/src/auth.js new file mode 100644 index 0000000..eb611bb --- /dev/null +++ b/src/auth.js @@ -0,0 +1,80 @@ +const axios = require('axios'); + +async function getDeviceCode(clientId) { + const params = new URLSearchParams({ + client_id: clientId, + scope: 'SDPOnDemand.requests.READ', + grant_type: 'device_request', + access_type: 'offline' + }); + + const res = await axios.post('https://accounts.zoho.com/oauth/v3/device/code', params); + return res.data; +} + +function pollForToken(clientId, clientSecret, deviceCode, interval, expiresIn) { + const pollParams = new URLSearchParams({ + client_id: clientId, + client_secret: clientSecret, + code: deviceCode, + grant_type: 'device_token' + }); + + return new Promise((resolve, reject) => { + const deadline = Date.now() + (expiresIn || 300) * 1000; + + const timer = setInterval(async () => { + if (Date.now() > deadline) { + clearInterval(timer); + reject(new Error('Expired try again.')); + return; + } + + try { + const res = await axios.post('https://accounts.zoho.com/oauth/v3/device/token', pollParams); + if (res.data.access_token) { + clearInterval(timer); + resolve(res.data.access_token); + } + } catch (err) { + } + }, interval + 500); + }); +} + +async function startAuthentication({ clientId, clientSecret, state }) { + state.auth = 'pending'; + state.lastError = null; + + const deviceData = await getDeviceCode(clientId); + state.verificationUrl = deviceData.verification_uri_complete; + state.userCode = deviceData.user_code; + + console.log('Open this URL and authorize:', deviceData.verification_uri_complete); + console.log('Code: ', deviceData.user_code); + + try { + const accessToken = await pollForToken( + clientId, + clientSecret, + deviceData.device_code, + deviceData.interval, + deviceData.expires_in + ); + + state.auth = 'connected'; + state.verificationUrl = null; + state.userCode = null; + return { accessToken }; + } catch (err) { + state.auth = 'error'; + state.lastError = err.message; + state.verificationUrl = null; + state.userCode = null; + return null; + } +} + +module.exports = { + startAuthentication +}; \ No newline at end of file diff --git a/src/boardService.js b/src/boardService.js new file mode 100644 index 0000000..bdc6aa6 --- /dev/null +++ b/src/boardService.js @@ -0,0 +1,89 @@ +const { startAuthentication } = require('./auth'); +const { fetchClosedTickets } = require('./tickets'); + +function createBoardService({ clientId, clientSecret, technicians, statusSet, windowMinutes, checkMinutes }) { + const state = { + auth: 'disconnected', + verificationUrl: null, + userCode: null, + lastCheck: null, + lastError: null + }; + + const tickets = new Map(); + let accessToken = null; + let pollingTimer = null; + + async function refreshTickets() { + if (!accessToken) return; + + await fetchClosedTickets({ + accessToken, + state, + tickets, + technicians, + statusSet, + windowMinutes + }); + } + + function startPolling() { + if (pollingTimer) return; + + pollingTimer = setInterval(() => { + refreshTickets().catch(err => { + state.lastError = err.message; + state.lastCheck = Date.now(); + }); + }, checkMinutes * 60 * 1000); + } + + async function connect() { + const authResult = await startAuthentication({ + clientId, + clientSecret, + state + }); + + if (!authResult?.accessToken) return; + + accessToken = authResult.accessToken; + console.log('Authorization successful'); + + await refreshTickets(); + startPolling(); + } + + function failConnection(err) { + state.auth = 'error'; + state.lastError = err.message; + state.verificationUrl = null; + state.userCode = null; + } + + function getState() { + return { + ...state, + windowMinutes, + checkMinutes, + watching: technicians, + serverTime: Date.now(), + tickets: [...tickets.values()].sort((a, b) => b.closedAt - a.closedAt) + }; + } + + async function checkNow() { + await refreshTickets(); + } + + return { + connect, + checkNow, + failConnection, + getState + }; +} + +module.exports = { + createBoardService +}; \ No newline at end of file diff --git a/src/env.example b/src/env.example new file mode 100644 index 0000000..b1fd72e --- /dev/null +++ b/src/env.example @@ -0,0 +1,4 @@ +CLIENT_ID= +CLIENT_SECRET= +TECHNICIAN_NAMES=First Last, First Last +STATUS_NAMES=Closed,Resolved \ No newline at end of file diff --git a/src/main.js b/src/main.js deleted file mode 100644 index 62492a8..0000000 --- a/src/main.js +++ /dev/null @@ -1,55 +0,0 @@ -/* SDP Request Watcher - * Monitors requests in Zoho's SDP Cloud platform to play a sound alert - * when a request is closed. - * - * Developed for MCCS ITS by Skylar Grant - */ - -const dotenv = require('dotenv'); -dotenv.config({ quiet: true }); -const { CLIENT_ID, CLIENT_SECRET } = process.env; -const { Client, ListInfoBuilder, SearchCriteriaBuilder } = require('./modules/sdp-api'); -const client = new Client(CLIENT_ID, CLIENT_SECRET); -const technicianNamesArray = process.env.TECHNICIAN_NAMES.split(','); -const statusNamesArray = process.env.STATUS_NAMES.split(','); - -async function authInitLoop() { - await client.init(); - console.log(`Please visit ${client.verificationUrl} and enter the code: ${client.userCode}`); - - // Poll for authorization - const pollInterval = setInterval(async () => { - try { - /** @type {boolean} */ - const authorized = await client.poll(); - if (authorized) { - clearInterval(pollInterval); - console.log('Authorization successful! Access token obtained.'); - } else { - console.log('Still waiting for user authorization...'); - } - } catch (error) { - clearInterval(pollInterval); - console.error('Error during polling:', error.message); - } - }, client.pollInterval + 500); -} - -async function pullRequests() { - try { - const listInfo = new ListInfoBuilder() - .setRowCount(10); - - const searchCriteria = new SearchCriteriaBuilder() - .addTechnicians(technicianNamesArray, "is") - .addStatuses(statusNamesArray, "is", "and"); - - listInfo.data.search_criteria = searchCriteria.data; - } catch (error) { - console.error('Error pulling requests:', error.message); - } -} - -authInitLoop().catch(err => { - console.error('Error in authInitLoop execution:', err.message); -}); \ No newline at end of file diff --git a/src/modules/sdp-api.js b/src/modules/sdp-api.js deleted file mode 100644 index 13d2582..0000000 --- a/src/modules/sdp-api.js +++ /dev/null @@ -1,231 +0,0 @@ -/* SDP-API Tool - * Client for interacting with the Zoho SDP Cloud API. - * - * Developed for MCCS ITS by Skylar Grant - */ -const axios = require('axios'); - -// Template strings for requests -const strings = { - tokenPrefix: 'Authorization: Zoho-oauthtoken ', - initiation: { - endpoint: 'https://accounts.zoho.com/oauth/v3/device/code', - params: { - grant_type: 'device_request', - scope: 'SDPOnDemand.requests.READ', - access_type: 'offline' - } - }, - polling: { - endpoint: 'https://accounts.zoho.com/oauth/v3/device/token', - params: { - grant_type: 'device_token' - } - } -} - -/** SDP API Client - * Handles OAuth device authorization flow for Zoho's SDP Cloud API. - * @class Client - * @constructor - * @param {string} clientId - The Client ID for the Zoho application. - * @param {string} clientSecret - The Client Secret for the Zoho application. - * @returns {Client} An instance of the SDP API Client. - */ -class Client { - constructor(clientId, clientSecret) { - if (!clientId || !clientSecret) { - throw new Error('Client ID and Client Secret are required to create an SDP API Client.'); - } - this.clientId = clientId; - this.clientSecret = clientSecret; - - // Set up placeholders for tokens and codes - this.deviceCode = null; - this.accessToken = null; - this.refreshToken = null; - this.userCode = null; - this.verificationUrl = null; - - // Create copies of the request parameter templates - this.initParams = JSON.parse(JSON.stringify(strings.initiation.params)); - this.initParams.client_id = this.clientId; - - this.pollParams = JSON.parse(JSON.stringify(strings.polling.params)); - this.pollParams.client_id = this.clientId; - this.pollParams.client_secret = this.clientSecret; - - return this; - } - - async init() { - const urlParams = new URLSearchParams(this.initParams).toString(); - const fullUrl = `${strings.initiation.endpoint}?${urlParams}`; - const initRes = await axios.post(fullUrl); - - if (initRes.status !== 200) { - throw new Error(`Error during initiation request: ${initRes.status} - ${initRes.statusText}`); - } - - this.deviceCode = initRes.data.device_code; - this.userCode = initRes.data.user_code; - this.verificationUrl = initRes.data.verification_uri_complete; - this.pollInterval = initRes.data.interval; - - return this; - } - - async poll() { - if (!this.deviceCode) { - throw new Error('Device code is not set. Please run the initiation request first.'); - } - this.pollParams.code = this.deviceCode; - const pollParams = new URLSearchParams(this.pollParams).toString(); - const fullPollUrl = `${strings.polling.endpoint}?${pollParams}`; - const pollRes = await axios.post(fullPollUrl); - if (pollRes.status !== 200) { - throw new Error(`Error during polling request: ${pollRes.status} - ${pollRes.statusText}`); - } - - if (pollRes.data.access_token) { - this.accessToken = pollRes.data.access_token; - this.refreshToken = pollRes.data.refresh_token; - return true; - } else { - return false; - } - } -} - -/** Search Criteria Builder - * Class to help build search criteria for SDP API requests. - * @class SearchCriteriaBuilder - * @constructor - * @returns {SearchCriteriaBuilder} An instance of the Search Criteria Builder. - */ -class SearchCriteriaBuilder { - constructor() { - this.data = new Array(); - return this; - } - - /** Add technician names to the search criteria. - * @param {Array} technicianNamesArray Array of technician names - * @param {String} condition Condition to use for filtering, "is" or "is not" (Default: "is") - * @param {String} operator Operator to use for filtering, "and" or "or" (Optional if first/only criterion) - * @returns {SearchCriteriaBuilder} - */ - addTechnicians(technicianNamesArray, condition, operator) { - if (!Array.isArray(technicianNamesArray) || technicianNamesArray.length === 0) { - throw new Error('Technician names must be a non-empty array.'); - } - condition = condition || 'is'; - - const newCriterion = { - field: 'technician.name', - condition: condition, - values: technicianNamesArray - }; - - if (operator) { - newCriterion.logical_operator = operator; - } - - this.data.push(newCriterion); - return this; - } - - /** Add status names to the search criteria. - * @param {Array} statusesArray Array of status names - * @param {String} condition Condition to use for filtering, "is" or "is not" (Default: "is not") - * @param {String} operator Operator to use for filtering, "and" or "or" (Optional if first/only criterion) - * @returns {SearchCriteriaBuilder} - */ - addStatuses(statusesArray, condition, operator) { - if (!Array.isArray(statusesArray) || statusesArray.length === 0) { - throw new Error('Statuses must be a non-empty array.'); - } - condition = condition || 'is not'; - - const newCriterion = { - field: 'status.internal_name', - condition: condition, - values: statusesArray - }; - if (operator) { - newCriterion.logical_operator = operator; - } - this.data.push(newCriterion); - return this; - } -} - -/** List Info Builder - * Class to help build list info for SDP API requests. - * @class ListInfoBuilder - * @constructor - * @returns {ListInfoBuilder} An instance of the List Info Builder. - */ -class ListInfoBuilder { - constructor() { - this.data = { - row_count: 100, - start_index: 0, - sort_field: 'last_updated_time', - sort_order: 'desc', - get_total_count: true - }; - return this; - } - - setRowCount(count) { - if (typeof count !== 'number' || count <= 0) { - throw new Error('Row count must be a positive number.'); - } - this.data.row_count = count; - return this; - } - - setStartIndex(index) { - if (typeof index !== 'number' || index < 0) { - throw new Error('Start index must be a non-negative number.'); - } - this.data.start_index = index; - return this; - } - - setSortField(field) { - if (typeof field !== 'string' || field.trim() === '') { - throw new Error('Sort field must be a non-empty string.'); - } - this.data.sort_field = field; - return this; - } - - setSortOrder(order) { - if (order !== 'asc' && order !== 'desc') { - throw new Error('Sort order must be either "asc" or "desc".'); - } - this.data.sort_order = order; - return this; - } - - enableTotalCount(enable) { - this.data.get_total_count = Boolean(enable); - return this; - } - - setSearchCriteria(criteriaArray) { - if (!Array.isArray(criteriaArray)) { - throw new Error('Search criteria must be an array.'); - } - this.data.search_criteria = criteriaArray; - return this; - } -} - -module.exports = { - Client, - SearchCriteriaBuilder, - ListInfoBuilder -} \ No newline at end of file diff --git a/src/public/ding.mp3 b/src/public/ding.mp3 new file mode 100644 index 0000000..f30e1bf Binary files /dev/null and b/src/public/ding.mp3 differ diff --git a/src/public/index.html b/src/public/index.html new file mode 100644 index 0000000..df8b266 --- /dev/null +++ b/src/public/index.html @@ -0,0 +1,481 @@ + + + + + +Closed ticket board + + + + + + +
+ +
+

Closed ticket board

+
+ Not connected +
+
+ +
+ +
+ +
+
+ + + + + + diff --git a/src/routes.js b/src/routes.js new file mode 100644 index 0000000..c79f9f9 --- /dev/null +++ b/src/routes.js @@ -0,0 +1,37 @@ +const express = require('express'); + +function createRoutes(boardService) { + const router = express.Router(); + + router.get('/state', (req, res) => { + res.json(boardService.getState()); + }); + + router.post('/connect', async (req, res) => { + const currentState = boardService.getState(); + if (currentState.auth === 'pending' || currentState.auth === 'connected') { + return res.json({ ok: true }); + } + + try { + boardService.connect().catch(err => { + boardService.failConnection(err); + }); + res.json({ ok: true }); + } catch (err) { + boardService.failConnection(err); + res.status(500).json({ ok: false, error: err.message }); + } + }); + + router.post('/check', async (req, res) => { + await boardService.checkNow(); + res.json({ ok: true }); + }); + + return router; +} + +module.exports = { + createRoutes +}; \ No newline at end of file diff --git a/src/server.js b/src/server.js new file mode 100644 index 0000000..9e7c620 --- /dev/null +++ b/src/server.js @@ -0,0 +1,43 @@ + +require('dotenv').config({ quiet: true }); + +const path = require('path'); +const express = require('express'); +const { createBoardService } = require('./boardService'); +const { createRoutes } = require('./routes'); + +const { CLIENT_ID, CLIENT_SECRET, TECHNICIAN_NAMES, STATUS_NAMES } = process.env; +const PORT = process.env.PORT || 3000; // default 3000 + +if (!CLIENT_ID || !CLIENT_SECRET) { + console.error('Client id and secret missing from .env'); + process.exit(1); +} + +const technicians = (TECHNICIAN_NAMES ? TECHNICIAN_NAMES.split(',') : []).map(t => t.trim()).filter(Boolean); +const statuses = (STATUS_NAMES ? STATUS_NAMES.split(',') : []).map(s => s.trim()).filter(Boolean); +const statusSet = new Set(statuses.map(s => s.toLowerCase())); + +const WINDOW_MINUTES = 10; // how far back the server looks in minutes +const CHECK_MINUTES = 1; // how often the server checks for new tickets in minutes + +const app = express(); + +//creates the board service with the config specified +const boardService = createBoardService({ + clientId: CLIENT_ID, + clientSecret: CLIENT_SECRET, + technicians, + statusSet, + windowMinutes: WINDOW_MINUTES, + checkMinutes: CHECK_MINUTES +}); + +app.use(express.static(path.join(__dirname, 'public'))); + +app.use('/api', createRoutes(boardService)); + +//starts listening on port specified +app.listen(PORT, () => { + console.log(`Open http://localhost:${PORT}`); +}); \ No newline at end of file diff --git a/src/tickets.js b/src/tickets.js new file mode 100644 index 0000000..0b85a25 --- /dev/null +++ b/src/tickets.js @@ -0,0 +1,104 @@ +const axios = require('axios'); + +function toMillis(completedTime) { + if (!completedTime) return null; + const raw = typeof completedTime === 'object' ? completedTime.value : completedTime; + const ms = Number(raw); + return Number.isFinite(ms) ? ms : null; +} + +function getRelevantTicketTime(ticket) { + return toMillis(ticket.completed_time) + || toMillis(ticket.resolved_time) + || toMillis(ticket.last_updated_time) + || null; +} + +async function fetchClosedTickets({ accessToken, state, tickets, technicians, statusSet, windowMinutes }) { + if (!accessToken) return; + + try { + const windowStart = Date.now() - windowMinutes * 60 * 1000; + + const listInfo = { + list_info: { + row_count: 200, + sort_field: 'last_updated_time', + sort_order: 'desc', + fields_required: [ + 'display_id', + 'subject', + 'status', + 'completed_time', + 'resolved_time', + 'last_updated_time', + 'technician' + ], + search_criteria: [ + { + field: 'last_updated_time', + condition: 'greater_than', + value: windowStart.toString() + } + ] + } + }; + + const response = await axios.get('https://ithelp.mainecc.edu/api/v3/requests', { + params: { input_data: JSON.stringify(listInfo) }, + headers: { + Authorization: `Zoho-oauthtoken ${accessToken}`, + Accept: 'application/vnd.manageengine.sdp.v3+json' + } + }); + + const found = response.data?.requests || []; + + for (const ticket of found) { + const statusName = ticket.status?.name || ''; + const statusInternalName = ticket.status?.internal_name || ''; + const statusMatches = !statusSet.size + || statusSet.has(statusName.toLowerCase()) + || statusSet.has(statusInternalName.toLowerCase()); + if (!statusMatches) continue; + + const closedAt = getRelevantTicketTime(ticket); + if (!closedAt || closedAt <= windowStart) continue; + + const technician = ticket.technician ? ticket.technician.name : null; + if (!technician) continue; + if (technicians.length && !technicians.includes(technician)) continue; + + tickets.set(String(ticket.display_id), { + id: ticket.display_id, + subject: ticket.subject, + technician, + status: statusName || null, + closedAt, + closedDisplay: ticket.completed_time?.display_value + || ticket.resolved_time?.display_value + || ticket.last_updated_time?.display_value + || null + }); + } + + const cutoff = Date.now() - 12 * 60 * 60 * 1000; + for (const [key, ticket] of tickets) { + if (ticket.closedAt < cutoff) tickets.delete(key); + } + + state.lastCheck = Date.now(); + state.lastError = null; + } catch (err) { + const detail = err.response?.data; + state.lastError = detail?.response_status?.messages?.[0]?.message + || (typeof detail === 'string' ? detail : null) + || err.message; + state.lastCheck = Date.now(); + console.error('Error: ', detail || err.message); + } +} + +module.exports = { + fetchClosedTickets +}; \ No newline at end of file