diff --git a/package-lock.json b/package-lock.json index 0561fa4..b2720ba 100644 --- a/package-lock.json +++ b/package-lock.json @@ -842,6 +842,14 @@ "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", "dev": true }, + "abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "requires": { + "event-target-shim": "^5.0.0" + } + }, "accepts": { "version": "1.3.7", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", @@ -887,6 +895,29 @@ "integrity": "sha512-7evsyfH1cLOCdAzZAd43Cic04yKydNx0cF+7tiA19p1XnLLPU4dpCQOqpjqwokFe//vS0QqfqqjCS2JkiIs0cA==", "dev": true }, + "agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "requires": { + "debug": "4" + }, + "dependencies": { + "debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, "ajv": { "version": "6.12.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.0.tgz", @@ -1022,6 +1053,11 @@ "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", "dev": true }, + "arrify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz", + "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==" + }, "asn1": { "version": "0.2.4", "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", @@ -1052,8 +1088,7 @@ "asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", - "dev": true + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" }, "atob": { "version": "2.1.2", @@ -1155,6 +1190,11 @@ } } }, + "base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" + }, "basic-auth": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", @@ -1179,6 +1219,11 @@ "tweetnacl": "^0.14.3" } }, + "bignumber.js": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.0.1.tgz", + "integrity": "sha512-IdZR9mh6ahOBv/hYGiXyVuyCetmGJhtYkqLBpTStdhEGjegpPlUawydyaF3pbIOFynJTpllEs+NP+CS9jKFLjA==" + }, "binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", @@ -1364,6 +1409,11 @@ "resolved": "https://registry.npmjs.org/bson/-/bson-1.1.5.tgz", "integrity": "sha512-kDuEzldR21lHciPQAIulLs1LZlCXdLziXI6Mb/TDkwXhb//UORJNPXgcRs2CuO4H0DcMkpfT3/ySsP3unoZjBg==" }, + "buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk=" + }, "buffer-from": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", @@ -1423,6 +1473,15 @@ } } }, + "call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "requires": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + } + }, "callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -1607,7 +1666,6 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, "requires": { "delayed-stream": "~1.0.0" } @@ -1615,8 +1673,7 @@ "component-emitter": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", - "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==", - "dev": true + "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==" }, "concat-map": { "version": "0.0.1", @@ -1685,6 +1742,11 @@ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" }, + "cookiejar": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.2.tgz", + "integrity": "sha512-Mw+adcfzPxcPeI+0WlvRrr/3lGVO0bD75SxX6811cxSh1Wbxx7xZBGK1eVtDf6si8rg2lhnUjsVLMFMfbRIuwA==" + }, "copy-descriptor": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", @@ -1925,8 +1987,7 @@ "delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", - "dev": true + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" }, "denque": { "version": "1.5.0", @@ -2017,6 +2078,14 @@ "safer-buffer": "^2.1.0" } }, + "ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "requires": { + "safe-buffer": "^5.0.1" + } + }, "ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -2451,6 +2520,11 @@ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" }, + "event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==" + }, "exec-sh": { "version": "0.3.4", "resolved": "https://registry.npmjs.org/exec-sh/-/exec-sh-0.3.4.tgz", @@ -2564,6 +2638,11 @@ } } }, + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, "extend-shallow": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", @@ -2693,6 +2772,16 @@ "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", "dev": true }, + "fast-safe-stringify": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.0.7.tgz", + "integrity": "sha512-Utm6CdzT+6xsDk2m8S6uL8VHxNwI6Jub+e9NYTcAms28T84pTa25GJQV9j0CY0N1rM8hK4x6grpF2BQf+2qwVA==" + }, + "fast-text-encoding": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/fast-text-encoding/-/fast-text-encoding-1.0.3.tgz", + "integrity": "sha512-dtm4QZH9nZtcDt8qJiOH9fcQd1NAgi+K1O2DbE6GG1PPCK/BWfOH3idCTRQ4ImXRUOyopDEgDEnVEE7Y/2Wrig==" + }, "fastq": { "version": "1.11.0", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.11.0.tgz", @@ -2807,6 +2896,11 @@ "mime-types": "^2.1.12" } }, + "formidable": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-1.2.2.tgz", + "integrity": "sha512-V8gLm+41I/8kguQ4/o1D3RIHRmhYFG4pnNyonvua+40rqcEmT4+V71yaZ3B457xbbgCsCfjSPi65u/W6vK1U5Q==" + }, "forwarded": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", @@ -2839,12 +2933,45 @@ "dev": true, "optional": true }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + }, "functional-red-black-tree": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", "dev": true }, + "gaxios": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-4.2.1.tgz", + "integrity": "sha512-s+rTywpw6CmfB8r9TXYkpix7YFeuRjnR/AqhaJrQqsNhsAqej+IAiCc3hadzQH3gHyWth30tvYjxH8EVjQt/8Q==", + "requires": { + "abort-controller": "^3.0.0", + "extend": "^3.0.2", + "https-proxy-agent": "^5.0.0", + "is-stream": "^2.0.0", + "node-fetch": "^2.3.0" + }, + "dependencies": { + "is-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz", + "integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==" + } + } + }, + "gcp-metadata": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-4.2.1.tgz", + "integrity": "sha512-tSk+REe5iq/N+K+SK1XjZJUrFPuDqGZVzCy2vocIHIGmPlTGsa8owXMJwGkrXr73NO0AzhPW4MF2DEHz7P2AVw==", + "requires": { + "gaxios": "^4.0.0", + "json-bigint": "^1.0.0" + } + }, "gensync": { "version": "1.0.0-beta.1", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.1.tgz", @@ -2857,6 +2984,16 @@ "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", "dev": true }, + "get-intrinsic": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", + "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==", + "requires": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1" + } + }, "get-stream": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", @@ -2933,6 +3070,30 @@ "slash": "^3.0.0" } }, + "google-auth-library": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-7.0.4.tgz", + "integrity": "sha512-o8irYyeijEiecTXeoEe8UKNEzV1X+uhR4b2oNdapDMZixypp0J+eHimGOyx5Joa3UAeokGngdtDLXtq9vDqG2Q==", + "requires": { + "arrify": "^2.0.0", + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "fast-text-encoding": "^1.0.0", + "gaxios": "^4.0.0", + "gcp-metadata": "^4.2.0", + "gtoken": "^5.0.4", + "jws": "^4.0.0", + "lru-cache": "^6.0.0" + } + }, + "google-p12-pem": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-3.0.3.tgz", + "integrity": "sha512-wS0ek4ZtFx/ACKYF3JhyGe5kzH7pgiQ7J5otlumqR9psmWMYc+U9cErKlCYVYHoUaidXHdZ2xbo34kB+S+24hA==", + "requires": { + "node-forge": "^0.10.0" + } + }, "got": { "version": "9.6.0", "resolved": "https://registry.npmjs.org/got/-/got-9.6.0.tgz", @@ -2965,6 +3126,16 @@ "dev": true, "optional": true }, + "gtoken": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-5.2.1.tgz", + "integrity": "sha512-OY0BfPKe3QnMsY9MzTHTSKn+Vl2l1CcLe6BwDEQj00mbbkl5nyQ/7EUREstg4fQNZ8iYE7br4JJ7TdKeDOPWmw==", + "requires": { + "gaxios": "^4.0.0", + "google-p12-pem": "^3.0.3", + "jws": "^4.0.0" + } + }, "har-schema": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", @@ -2981,12 +3152,25 @@ "har-schema": "^2.0.0" } }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "requires": { + "function-bind": "^1.1.1" + } + }, "has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", "dev": true }, + "has-symbols": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz", + "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==" + }, "has-value": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", @@ -3101,6 +3285,30 @@ "resolved": "https://registry.npmjs.org/https/-/https-1.0.0.tgz", "integrity": "sha1-PDfHrhqO65ZpBKKtHpdaGUt+06Q=" }, + "https-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz", + "integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==", + "requires": { + "agent-base": "6", + "debug": "4" + }, + "dependencies": { + "debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, "human-signals": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz", @@ -5361,6 +5569,14 @@ "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", "dev": true }, + "json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "requires": { + "bignumber.js": "^9.0.0" + } + }, "json-buffer": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.0.tgz", @@ -5400,6 +5616,49 @@ "minimist": "^1.2.0" } }, + "jsonwebtoken": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz", + "integrity": "sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w==", + "requires": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^5.6.0" + }, + "dependencies": { + "jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "requires": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "requires": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + } + } + }, "jsprim": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", @@ -5412,6 +5671,25 @@ "verror": "1.10.0" } }, + "jwa": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", + "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", + "requires": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "jws": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", + "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "requires": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + } + }, "keyv": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/keyv/-/keyv-3.1.0.tgz", @@ -5473,12 +5751,47 @@ "integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==", "dev": true }, + "lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8=" + }, + "lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY=" + }, + "lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha1-YZwK89A/iwTDH1iChAt3sRzWg0M=" + }, + "lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha1-POdoEMWSjQM1IwGsKHMX8RwLH/w=" + }, + "lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=" + }, + "lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha1-1SfftUVuynzJu5XV2ur4i6VKVFE=" + }, "lodash.memoize": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", "integrity": "sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=", "dev": true }, + "lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha1-DdOXEhPHxW34gJd9UEyI+0cal6w=" + }, "lodash.sortby": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", @@ -5509,7 +5822,6 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, "requires": { "yallist": "^4.0.0" } @@ -5603,6 +5915,11 @@ "picomatch": "^2.0.5" } }, + "mime": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.5.2.tgz", + "integrity": "sha512-tqkh47FzKeCPD2PUiPB6pkbMzsCasjxAfC62/Wap5qrUWcb+sFasXUC5I3gYM5iBM8v/Qpn4UK0x+j0iHyFPDg==" + }, "mime-db": { "version": "1.43.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.43.0.tgz", @@ -5773,6 +6090,16 @@ "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", "dev": true }, + "node-fetch": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", + "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==" + }, + "node-forge": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.10.0.tgz", + "integrity": "sha512-PPmu8eEeG9saEUvI97fm4OYxXVB6bFvyNTyiUOBichBpFG8A1Ljw3bY62+5oOjDEMHRnd0Y7HQ+x7uzxOzC6JA==" + }, "node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -5914,6 +6241,11 @@ } } }, + "object-inspect": { + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.10.3.tgz", + "integrity": "sha512-e5mCJlSH7poANfC8z8S9s9S2IN5/4Zb3aZ33f5s8YqoazCFzNLloLU8r5VCG+G7WoqLvAAZoVMcy3tp/3X0Plw==" + }, "object-keys": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", @@ -6933,6 +7265,16 @@ "dev": true, "optional": true }, + "side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "requires": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + } + }, "signal-exit": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", @@ -7349,6 +7691,84 @@ "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", "dev": true }, + "superagent": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-6.1.0.tgz", + "integrity": "sha512-OUDHEssirmplo3F+1HWKUrUjvnQuA+nZI6i/JJBdXb5eq9IyEQwPyPpqND+SSsxf6TygpBEkUjISVRN4/VOpeg==", + "requires": { + "component-emitter": "^1.3.0", + "cookiejar": "^2.1.2", + "debug": "^4.1.1", + "fast-safe-stringify": "^2.0.7", + "form-data": "^3.0.0", + "formidable": "^1.2.2", + "methods": "^1.1.2", + "mime": "^2.4.6", + "qs": "^6.9.4", + "readable-stream": "^3.6.0", + "semver": "^7.3.2" + }, + "dependencies": { + "debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "requires": { + "ms": "2.1.2" + } + }, + "form-data": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz", + "integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "qs": { + "version": "6.10.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.1.tgz", + "integrity": "sha512-M528Hph6wsSVOBiYUnGf+K/7w0hNshs/duGsNXPUCLH5XAqjEtiPGwNONLV0tBH8NoGb0mvD5JubnUTrujKDTg==", + "requires": { + "side-channel": "^1.0.4" + } + }, + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "semver": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", + "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "requires": { + "lru-cache": "^6.0.0" + } + } + } + }, + "supertest": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/supertest/-/supertest-6.1.3.tgz", + "integrity": "sha512-v2NVRyP73XDewKb65adz+yug1XMtmvij63qIWHZzSX8tp6wiq6xBLUy4SUAd2NII6wIipOmHT/FD9eicpJwdgQ==", + "requires": { + "methods": "^1.1.2", + "superagent": "^6.1.0" + } + }, "supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", @@ -8169,8 +8589,7 @@ "yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, "yn": { "version": "3.1.1", diff --git a/package.json b/package.json index 157bbe1..0c6567f 100644 --- a/package.json +++ b/package.json @@ -39,13 +39,16 @@ "eslint-config-prettier": "^8.3.0", "eslint-plugin-prettier": "^3.4.0", "express": "^4.17.1", + "google-auth-library": "^7.0.4", "https": "^1.0.0", "i18next": "^19.9.0", "jest-coverage-badges": "^1.1.2", + "jsonwebtoken": "^8.5.1", "mongodb": "^3.6.3", "morgan": "^1.10.0", "node-schedule": "^1.3.2", "prettier": "^2.2.1", + "supertest": "^6.1.3", "ts-node": "^9.1.1", "typescript": "^3.8.2" }, diff --git a/src/actions/interactive.ts b/src/actions/interactive.ts index 486766f..999701e 100644 --- a/src/actions/interactive.ts +++ b/src/actions/interactive.ts @@ -22,7 +22,7 @@ export const interactive = (platform: Platform, props: InteractiveProps): void = ["accept-coffee"]: acceptCoffee, ["reject-coffee"]: rejectCoffee, ["try-again-coffee"]: tryAgainCoffee, - ["stop-coffee"]: stopCoffee + ["stop-coffee"]: stopCoffee, } const command = mapper[nextStep]; diff --git a/src/actions/register/register-confirmation.ts b/src/actions/register/register-confirmation.ts new file mode 100644 index 0000000..f74a0d1 --- /dev/null +++ b/src/actions/register/register-confirmation.ts @@ -0,0 +1,21 @@ +import { Platform } from "../../services/platform/platform"; +import { Database } from "../../services/database/database"; +import { I18n } from "../../services/i18n/i18n"; +import { Logger } from "../../services/logger/logger"; +import { RegisterProps } from "./register"; + +export const registerConfirmation = async ( + platform: Platform, + { userId, userName }: RegisterProps, + db: Database = Database.make() +): Promise => { + const i18n: I18n = await I18n.getInstance() + const senderId = userId + const savedUser = await db.saveUser({ userId: userId, userName: userName }) + if (savedUser) { + await platform.sendMessage(senderId, i18n.translate("register.success")) + Logger.log(`Registered user: { userId: ${savedUser.userId}, userName: ${savedUser.userName}`) + } else { + await platform.sendMessage(senderId, i18n.translate("register.failure")) + } +} \ No newline at end of file diff --git a/src/actions/register/register.ts b/src/actions/register/register.ts new file mode 100644 index 0000000..7430971 --- /dev/null +++ b/src/actions/register/register.ts @@ -0,0 +1,20 @@ +import { Platform } from "../../services/platform/platform"; +import { Database } from "../../services/database/database"; +import * as jwt from "jsonwebtoken"; + +export interface RegisterProps { + userId: string + userName: string, +} + +export const register = async (platform: Platform, data: RegisterProps): Promise => { + const db: Database = Database.make() + const user = await db.getUser(data.userId) + console.log(user) + if (user) { + await platform.sendMessage(data.userId, "user already exists") + } else { + const token = jwt.sign(data, process.env.PASS) + await platform.sendMessage(data.userId, `http://localhost:3000/signup?id=${token}`) + } +} diff --git a/src/models/database/dtos/user-dto.ts b/src/models/database/dtos/user-dto.ts new file mode 100644 index 0000000..073c2ac --- /dev/null +++ b/src/models/database/dtos/user-dto.ts @@ -0,0 +1,37 @@ +import { User } from "../user"; +import { JsonData } from "../../json-data"; + +export class UserDto { + private constructor ( + private userId: string, + private userName: string, + ) { } + + static fromJson(data: JsonData): UserDto { + return new UserDto( + data.userId, + data.userName + ) + } + + toJson(): JsonData { + return { + userId: this.userId, + userName: this.userName + } + } + + static fromModel(model: User): UserDto { + return new UserDto( + model.userId, + model.userName + ) + } + + toModel(): User { + return new User( + this.userId, + this.userName + ) + } +} \ No newline at end of file diff --git a/src/models/database/gratitude-message.ts b/src/models/database/gratitude-message.ts index ada7964..3851b25 100644 --- a/src/models/database/gratitude-message.ts +++ b/src/models/database/gratitude-message.ts @@ -24,8 +24,4 @@ export interface GratitudeSummary { user: Id, sent: GratitudeSummaryMessage[], received: GratitudeSummaryMessage[], -} - -export interface GratitudeMessageOptions { - days?: number } \ No newline at end of file diff --git a/src/models/database/user.ts b/src/models/database/user.ts new file mode 100644 index 0000000..4e5e8a2 --- /dev/null +++ b/src/models/database/user.ts @@ -0,0 +1,6 @@ +export class User { + constructor( + public userId: string, + public userName: string + ) { } +} \ No newline at end of file diff --git a/src/services/api/api.spec.ts b/src/services/api/api.spec.ts new file mode 100644 index 0000000..1343306 --- /dev/null +++ b/src/services/api/api.spec.ts @@ -0,0 +1,25 @@ +import * as supertest from "supertest" +import { httpsApp } from "./api"; + +const request = supertest(httpsApp) + +describe('API', () => { + it.todo('POST interactive', async done => { + const response = await request.post('/interactive') + expect(response.status).toBe(200) + done() + }) + + it.todo('POST thanks', async done => { + const response = await request.post('/thanks') + expect(response.status).toBe(200) + done() + }) + + it.todo('POST coffeeRoulette', async done => { + const response = await request.post('/coffeeRoulette') + expect(response.status).toBe(200) + done() + }) +}) + diff --git a/src/services/api/api.ts b/src/services/api/api.ts index 026abec..0568380 100644 --- a/src/services/api/api.ts +++ b/src/services/api/api.ts @@ -1,5 +1,6 @@ import * as https from "https" import * as fs from "fs" +import * as jwt from "jsonwebtoken"; import { json, urlencoded } from "body-parser" import { getDateFormatted, Logger } from "../logger/logger" import { config } from "../../config" @@ -8,15 +9,45 @@ import { Emojis } from "../../models/emojis" import * as morgan from "morgan" import { registerCommunity } from "./methods/register-community" import { getPlatformData } from "./methods/get-platform-data" +import { Slack } from "../platform/slack/slack" +import { Database } from "../database/database" +import { QueryOptions } from "../database/mongo/methods/query" +import { OAuth2Client } from "google-auth-library" + +const decodeGoogleToken = async (client: OAuth2Client, request: any) => { + const authorization: string = request.get("authorization") + const bearer = "bearer " + if (authorization && authorization.toLowerCase().startsWith(bearer)) { + const token = authorization.substring(bearer.length) + return await client.verifyIdToken({ + idToken: token, + audience: process.env.CLIENT_ID + }).then(ticket => ticket.getPayload()) + } +} + +const decodeIdToken = (token: string) => { + console.log(token) + return jwt.verify(token, process.env.PASS, function (error, decode){ + if(error){ + throw new Error(error) + } + return decode + }) +} + +interface SignUpQuery { + id: string +} // eslint-disable-next-line @typescript-eslint/no-var-requires const app = require("express")() +const client = new OAuth2Client(process.env.CLIENT_ID) + app.use(json()) app.use(urlencoded({ extended: true })) -morgan.token("date", () => { - return getDateFormatted() -}) +morgan.token("date", () => getDateFormatted()) app.use(morgan(":date :method :url :status :res[content-length] - :response-time ms")) Endpoints.forEach(({ name, action, getProps }: EndpointInstance) => { @@ -40,11 +71,49 @@ Endpoints.forEach(({ name, action, getProps }: EndpointInstance) => { }) }) -export const httpsApp = https.createServer( - { - key: fs.readFileSync(process.env.HTTPS_KEY ?? ""), - cert: fs.readFileSync(process.env.HTTPS_CERT ?? ""), - ca: fs.readFileSync(process.env.HTTPS_CHAIN ?? ""), - }, - app -) +const db: Database = Database.make() + +app.get("/gratitudeMessages", async (request: any, response: any) => { + // TODO: maintenance?? + const options: QueryOptions = request.query + const gratitudeMessages = await db.getGratitudeMessages(options) + return response.send(gratitudeMessages) +}) + +app.get("/coffeeBreaks", async (request: any, response: any) => { + const options: QueryOptions = request.query + const coffeeBreaks = await db.getCoffeeBreaks(options) + return response.send(coffeeBreaks) +}) + +app.get("/users/:id", async (request: any, response: any) => { + const userId: string = request.params.id + const { id, name } = await Slack.getInstance().getUserInfo(userId) ?? { id: "", name: ""} + return response.send({ id, name }) +}) + +app.post("/auth", async (request: any, response: any) => { + const decodedToken = await decodeGoogleToken(client, request); + console.log("/auth <--", decodedToken) + + return response.send() +}) + +app.post("/signup", async (request: any, response: any) => { + const options: SignUpQuery = request.query + try { + const decodedGoogleToken = await decodeGoogleToken(client, request) + const decodedIdToken = decodeIdToken(options.id) + console.log("/signup <--", decodedGoogleToken, decodedIdToken) + return response.send() + } catch (error) { + console.log(error) + return response.send(`Invalid token: ${error}`) + } +}) + +export const httpsApp = https.createServer({ + key: fs.readFileSync(process.env.HTTPS_KEY ?? ""), + cert: fs.readFileSync(process.env.HTTPS_CERT ?? ""), + ca: fs.readFileSync(process.env.HTTPS_CHAIN ?? "") +}, app) diff --git a/src/services/api/auth.ts b/src/services/api/auth.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/services/api/endpoints.ts b/src/services/api/endpoints.ts index d161664..7d89263 100644 --- a/src/services/api/endpoints.ts +++ b/src/services/api/endpoints.ts @@ -3,6 +3,7 @@ import { interactive } from "../../actions/interactive" import { sendGratitudeSummaries } from "../../actions/thanks" import { thanks } from "../../actions/thanks" import { Platform } from "../platform/platform" +import { register } from "../../actions/register/register" export interface EndpointInstance { name: Endpoint @@ -15,6 +16,7 @@ export enum Endpoint { thanks = "/thanks", coffeeRoulette = "/coffee-roulette", sendSummary = "/send-summary", + register = "/register" } export const Endpoints: EndpointInstance[] = [ @@ -38,4 +40,9 @@ export const Endpoints: EndpointInstance[] = [ action: (_) => sendGratitudeSummaries(), getProps: async () => undefined, }, + { + name: Endpoint.register, + action: register, + getProps: (platform, data) => platform.getRegisterProps(data) + } ] diff --git a/src/services/database/database.ts b/src/services/database/database.ts index e874412..c9cfca8 100644 --- a/src/services/database/database.ts +++ b/src/services/database/database.ts @@ -1,6 +1,8 @@ +import { GratitudeMessage } from "../../models/database/gratitude-message" +import { QueryOptions } from "./mongo/methods/query" +import { User } from "../../models/database/user" import { CoffeeBreak } from "../../models/database/coffee-break" import { Community } from "../../models/database/community" -import { GratitudeMessage, GratitudeMessageOptions } from "../../models/database/gratitude-message" import { Logger } from "../logger/logger" export type DatabaseName = "mongo" @@ -27,7 +29,12 @@ export abstract class Database { abstract getCommunities: () => Promise abstract saveGratitudeMessages: (gratitudeMessages: GratitudeMessage[]) => Promise - abstract getGratitudeMessages: (options: GratitudeMessageOptions) => Promise + abstract getGratitudeMessages: (options: QueryOptions) => Promise abstract saveCoffeeBreak: (coffeeBreak: CoffeeBreak) => Promise + abstract getCoffeeBreaks: (options: QueryOptions) => Promise + + abstract saveUser: (user: User) => Promise + abstract getUser: (userId: string) => Promise + } diff --git a/src/services/database/mongo/collection.ts b/src/services/database/mongo/collection.ts index b79f358..044db4a 100644 --- a/src/services/database/mongo/collection.ts +++ b/src/services/database/mongo/collection.ts @@ -2,4 +2,5 @@ export enum Collection { communities = "communities", gratitudeMessages = "communities.gratitudemessages", coffeeBreaks = "communities.coffeebreaks", + users = "users" } diff --git a/src/services/database/mongo/methods/query.ts b/src/services/database/mongo/methods/query.ts new file mode 100644 index 0000000..52b6477 --- /dev/null +++ b/src/services/database/mongo/methods/query.ts @@ -0,0 +1,45 @@ +export interface QueryOptions { + communityId?: string, + days?: number, + userId?: string, + startDate?: string, + endDate?: string +} + +const queryDays = (query: any, days: number): any => { + const nowTime = (new Date()).getTime() + const queryTime = days * 24 * 60 * 60 * 1000 + query["createdAtTime"] = { + $gte: nowTime - queryTime, + $lt: nowTime, + } + return query +} + +export const makeQuery = (options: QueryOptions): any => { + const query = {} + + if (options.communityId) { + query["communityId"] = options.communityId + } + + if (options.days) { + queryDays(query, options.days) + } else if (options.startDate || options.endDate) { + query["createdAtTime"] = {} + if (options.startDate) { + query["createdAtTime"]["$gte"] = (new Date(options.startDate)).getTime() + } + if (options.endDate) { + query["createdAtTime"]["$lt"] = (new Date(options.endDate)).getTime() + } + } + + if (options.userId) { + query["$or"] = [ + { senderId: options.userId }, + { recipientId: options.userId } + ] + } + return query +} \ No newline at end of file diff --git a/src/services/database/mongo/mongo.spec.ts b/src/services/database/mongo/mongo.spec.ts index afec12f..90e8c29 100644 --- a/src/services/database/mongo/mongo.spec.ts +++ b/src/services/database/mongo/mongo.spec.ts @@ -6,6 +6,9 @@ import { Community } from "../../../models/database/community" import { config } from "../../../config" import { MongoDB } from "./mongo" import { GratitudeMessage } from "../../../models/database/gratitude-message" +import { Id } from "../../../models/platform/slack/id" +import { User } from "../../../models/database/user" +import { UserBuilder } from "../../../tests/builders/models/user-builder" describe("Service MongoDB: ", () => { let db: MongoDB @@ -58,6 +61,28 @@ describe("Service MongoDB: ", () => { }) }) + describe('Collection users', () => { + it('should save and retrieve user', async () => { + const user: User = UserBuilder({}) + await db.saveUser(user) + const retrievedUser: User | undefined = await db.getUser(user.userId) + + expect(retrievedUser).not.toBeUndefined() + expect(retrievedUser).toEqual(user) + }) + it('should not save user when it already exists', async () => { + const user: User = UserBuilder({userName: 'repeated-username'}) + + const firstInsertedUser: User | undefined = await db.saveUser(user) + const retrievedUser: User | undefined = await db.getUser(user.userId) + const secondInsertedUser: User | undefined = await db.saveUser(user) + + expect(firstInsertedUser).toEqual(user) + expect(secondInsertedUser).toBeUndefined() + expect(retrievedUser).not.toBeUndefined() + }) + }) + describe("Collection gratitude messages:", () => { it("should save and retrieve gratitude messages", async () => { const gratitudeMessages: GratitudeMessage[] = [ @@ -75,6 +100,38 @@ describe("Service MongoDB: ", () => { expect(retrievedMessages).toHaveLength(3) }) + it("should retrieve gratitude messages for a certain community", async () => { + const communityId = "test-community-id" + const gratitudeMessages: GratitudeMessage[] = [ + GratitudeMessageBuilder({ communityId }), + GratitudeMessageBuilder({}), + ] + await db.saveGratitudeMessages(gratitudeMessages) + + const retrievedMessages: GratitudeMessage[] = await db.getGratitudeMessages({ communityId }) + + expect(retrievedMessages).toContainEqual(gratitudeMessages[0]) + expect(retrievedMessages).not.toContainEqual(gratitudeMessages[1]) + expect(retrievedMessages).toHaveLength(1) + }) + + it("should retrieve gratitude messages for a certain user", async () => { + const userId: Id = new Id("test-user-id") + const gratitudeMessages: GratitudeMessage[] = [ + GratitudeMessageBuilder({ sender: userId }), + GratitudeMessageBuilder({ recipient: userId }), + GratitudeMessageBuilder({}) + ] + await db.saveGratitudeMessages(gratitudeMessages) + + const retrievedMessages: GratitudeMessage[] = await db.getGratitudeMessages({ userId: userId.id }) + + expect(retrievedMessages).toContainEqual(gratitudeMessages[0]) + expect(retrievedMessages).toContainEqual(gratitudeMessages[1]) + expect(retrievedMessages).not.toContainEqual(gratitudeMessages[2]) + expect(retrievedMessages).toHaveLength(2) + }) + it("should retrieve gratitude messages from a given number of days", async () => { const today = new Date() const fiveDaysAgo = new Date() @@ -92,12 +149,148 @@ describe("Service MongoDB: ", () => { expect(retrievedMessages).not.toContainEqual(gratitudeMessages[1]) expect(retrievedMessages).toHaveLength(1) }) + + it("should retrieve gratitude messages for a given time interval with 1 or both boundaries", async () => { + const today = new Date() + const fiveDaysAgo = new Date(today.getDate() - 5) + const tenDaysAgo = new Date(today.getDate() - 10) + + const gratitudeMessages: GratitudeMessage[] = [ + GratitudeMessageBuilder({ createdAt: today }), + GratitudeMessageBuilder({ createdAt: fiveDaysAgo }), + GratitudeMessageBuilder({ createdAt: tenDaysAgo }) + ] + await db.saveGratitudeMessages(gratitudeMessages) + + const startDate = new Date(today.getDate() - 7).toISOString() + const endDate = new Date(today.getDate() - 2).toISOString() + + let retrievedMessages: GratitudeMessage[] = await db.getGratitudeMessages({ startDate, endDate }) + + expect(retrievedMessages).not.toContainEqual(gratitudeMessages[0]) + expect(retrievedMessages).toContainEqual(gratitudeMessages[1]) + expect(retrievedMessages).not.toContainEqual(gratitudeMessages[2]) + expect(retrievedMessages).toHaveLength(1) + + retrievedMessages = await db.getGratitudeMessages({ startDate }) + expect(retrievedMessages).toContainEqual(gratitudeMessages[0]) + expect(retrievedMessages).toContainEqual(gratitudeMessages[1]) + expect(retrievedMessages).not.toContainEqual(gratitudeMessages[2]) + expect(retrievedMessages).toHaveLength(2) + + retrievedMessages = await db.getGratitudeMessages({ endDate }) + expect(retrievedMessages).not.toContainEqual(gratitudeMessages[0]) + expect(retrievedMessages).toContainEqual(gratitudeMessages[1]) + expect(retrievedMessages).toContainEqual(gratitudeMessages[2]) + expect(retrievedMessages).toHaveLength(2) + }) }) describe("Collection coffee breaks:", () => { - it("should save coffee breaks", async () => { + it("should save and retrieve coffee breaks", async () => { const coffeeBreak: CoffeeBreak = CoffeeBreakBuilder({}) + await db.saveCoffeeBreak(coffeeBreak) + + const retrievedCoffees: CoffeeBreak[] = await db.getCoffeeBreaks({}) + + expect(retrievedCoffees).toContainEqual(coffeeBreak) + expect(retrievedCoffees).toHaveLength(1) + }) + + it("should retrieve coffee breaks for a certain community", async () => { + const communityId = "test-community-id" + const coffeeBreaks: CoffeeBreak[] = [ + CoffeeBreakBuilder({ communityId }), + CoffeeBreakBuilder({}) + ] + + for (const coffeeBreak of coffeeBreaks) { + await db.saveCoffeeBreak(coffeeBreak) + } + + const retrievedCoffees: CoffeeBreak[] = await db.getCoffeeBreaks({ communityId }) + + expect(retrievedCoffees).toContainEqual(coffeeBreaks[0]) + expect(retrievedCoffees).not.toContainEqual(coffeeBreaks[1]) + expect(retrievedCoffees).toHaveLength(1) + }) + + it("should retrieve coffee breaks for a certain user", async () => { + const userId: Id = new Id("test-user-id") + const coffeeBreaks: CoffeeBreak[] = [ + CoffeeBreakBuilder({ sender: userId }), + CoffeeBreakBuilder({ recipient: userId }), + CoffeeBreakBuilder({}) + ] + + for (const coffeeBreak of coffeeBreaks) { + await db.saveCoffeeBreak(coffeeBreak) + } + + const retrievedCoffees: CoffeeBreak[] = await db.getCoffeeBreaks({ userId: userId.id }) + + expect(retrievedCoffees).toContainEqual(coffeeBreaks[0]) + expect(retrievedCoffees).toContainEqual(coffeeBreaks[1]) + expect(retrievedCoffees).not.toContainEqual(coffeeBreaks[2]) + expect(retrievedCoffees).toHaveLength(2) + }) + + it("should retrieve coffee breaks from a given number of days", async () => { + const today = new Date() + const fiveDaysAgo = new Date(today.getDate() - 5) + + const coffeeBreaks: CoffeeBreak[] = [ + CoffeeBreakBuilder({ createdAt: today }), + CoffeeBreakBuilder({ createdAt: fiveDaysAgo }) + ] + + for (const coffeeBreak of coffeeBreaks) { + await db.saveCoffeeBreak(coffeeBreak) + } + + const retrievedCoffees: CoffeeBreak[] = await db.getCoffeeBreaks({ days: 3 }) + + expect(retrievedCoffees).toContainEqual(coffeeBreaks[0]) + expect(retrievedCoffees).not.toContainEqual(coffeeBreaks[1]) + expect(retrievedCoffees).toHaveLength(1) + }) + + it("should retrieve coffee breaks for a given time interval with 1 or both boundaries", async () => { + const today = new Date() + const fiveDaysAgo = new Date(today.getDate() - 5) + const tenDaysAgo = new Date(today.getDate() - 10) + + const coffeeBreaks: CoffeeBreak[] = [ + CoffeeBreakBuilder({ createdAt: today }), + CoffeeBreakBuilder({ createdAt: fiveDaysAgo }), + CoffeeBreakBuilder({ createdAt: tenDaysAgo }) + ] + for (const coffeeBreak of coffeeBreaks) { + await db.saveCoffeeBreak(coffeeBreak) + } + + const startDate = new Date(today.getDate() - 7).toISOString() + const endDate = new Date(today.getDate() - 2).toISOString() + + let retrievedCoffees: CoffeeBreak[] = await db.getCoffeeBreaks({ startDate, endDate }) + + expect(retrievedCoffees).not.toContainEqual(coffeeBreaks[0]) + expect(retrievedCoffees).toContainEqual(coffeeBreaks[1]) + expect(retrievedCoffees).not.toContainEqual(coffeeBreaks[2]) + expect(retrievedCoffees).toHaveLength(1) + + retrievedCoffees = await db.getCoffeeBreaks({ startDate }) + expect(retrievedCoffees).toContainEqual(coffeeBreaks[0]) + expect(retrievedCoffees).toContainEqual(coffeeBreaks[1]) + expect(retrievedCoffees).not.toContainEqual(coffeeBreaks[2]) + expect(retrievedCoffees).toHaveLength(2) + + retrievedCoffees = await db.getCoffeeBreaks({ endDate }) + expect(retrievedCoffees).not.toContainEqual(coffeeBreaks[0]) + expect(retrievedCoffees).toContainEqual(coffeeBreaks[1]) + expect(retrievedCoffees).toContainEqual(coffeeBreaks[2]) + expect(retrievedCoffees).toHaveLength(2) }) }) }) diff --git a/src/services/database/mongo/mongo.ts b/src/services/database/mongo/mongo.ts index 26b9a02..8d1e1e2 100644 --- a/src/services/database/mongo/mongo.ts +++ b/src/services/database/mongo/mongo.ts @@ -7,9 +7,12 @@ import { Collection } from "./collection" import { config } from "../../../config" import { Community } from "../../../models/database/community" import { CommunityDto } from "../../../models/database/dtos/community-dto" -import { GratitudeMessage, GratitudeMessageOptions } from "../../../models/database/gratitude-message" +import { GratitudeMessage } from "../../../models/database/gratitude-message" import { GratitudeMessageDto } from "../../../models/database/dtos/gratitude-message-dto" import { CoffeeBreakDto } from "../../../models/database/dtos/coffee-break-dto" +import { makeQuery, QueryOptions } from "./methods/query" +import { User } from "../../../models/database/user" +import { UserDto } from "../../../models/database/dtos/user-dto" export class MongoDB extends Database { private database = config.database.mongodb.database @@ -66,7 +69,7 @@ export class MongoDB extends Database { await this.on(async () => await this.insertGratitudeMessages(gratitudeMessages)) } - getGratitudeMessages = async (options: GratitudeMessageOptions): Promise => { + getGratitudeMessages = async (options: QueryOptions): Promise => { Logger.onDBAction("Getting gratitude messages") return await this.on(async () => await this.findGratitudeMessages(options)) } @@ -76,6 +79,21 @@ export class MongoDB extends Database { return await this.on(async () => await this.insertCoffeeBreak(coffeeBreak)) } + getCoffeeBreaks = async (options: QueryOptions): Promise => { + Logger.onDBAction("Getting coffee breaks...") + return await this.on(async () => await this.findCoffeeBreaks(options)) + } + + saveUser = async (user: User): Promise => { + Logger.onDBAction("Registering user") + return await this.on(async () => await this.insertUser(user)) + } + + getUser = async(userId: string): Promise => { + Logger.onDBAction(`Getting user with id ${userId}`) + return await this.on(async () => await this.getUserById(userId)) + } + private dropDatabase = () => this.instance.db(this.database).dropDatabase() private insertCommunity = async (community: Community) => { @@ -105,7 +123,7 @@ export class MongoDB extends Database { } } - private findGratitudeMessages = async (options: GratitudeMessageOptions) => { + private findGratitudeMessages = async (options: QueryOptions) => { const query = {} if (options.days) { const nowTime = new Date().getTime() @@ -125,4 +143,26 @@ export class MongoDB extends Database { const coffeeBreakJson = CoffeeBreakDto.fromModel(coffeeBreak).toJson() await this.instance.db(this.database).collection(Collection.coffeeBreaks).insertOne(coffeeBreakJson) } -} + + private findCoffeeBreaks = async (options: QueryOptions) => { + const query = makeQuery(options) + const cursor = await this.instance.db(this.database).collection(Collection.coffeeBreaks).find(query).toArray() + return cursor.map((coffeeBreakJson): CoffeeBreak => CoffeeBreakDto.fromJson(coffeeBreakJson).toModel()) + } + + private getUserById = async (userId: string): Promise => { + const usersCollection = this.instance.db(this.database).collection(Collection.users) + const user = await usersCollection.findOne({"userId": userId}) + return user ? UserDto.fromJson(user).toModel() : undefined + } + + private insertUser = async (user: User) => { + const userExists = await this.getUserById(user.userId) + if (userExists) { + return undefined + } + const userJson = UserDto.fromModel(user).toJson() + await this.instance.db(this.database).collection(Collection.users).insertOne(userJson) + return user + } +} \ No newline at end of file diff --git a/src/services/i18n/translations/en.json b/src/services/i18n/translations/en.json index 145c6e7..6a71d0b 100644 --- a/src/services/i18n/translations/en.json +++ b/src/services/i18n/translations/en.json @@ -51,5 +51,9 @@ "reject": "Reject", "yes": "Yes", "no": "No" + }, + "register": { + "success": "New user registered successfully", + "failure": "Couldn't register user" } } \ No newline at end of file diff --git a/src/services/i18n/translations/es.can.json b/src/services/i18n/translations/es.can.json index 8479f35..331df2c 100644 --- a/src/services/i18n/translations/es.can.json +++ b/src/services/i18n/translations/es.can.json @@ -51,5 +51,9 @@ "reject": "Quita pa'llá", "yes": "Sobran", "no": "Pasando" + }, + "register": { + "success": "Usuario registrado", + "failure": "No se pudo registrar al usuario" } } \ No newline at end of file diff --git a/src/services/i18n/translations/es.json b/src/services/i18n/translations/es.json index 75e1a4e..1086aed 100644 --- a/src/services/i18n/translations/es.json +++ b/src/services/i18n/translations/es.json @@ -51,5 +51,9 @@ "reject": "Rechazar", "yes": "Sí", "no": "No" + }, + "register": { + "success": "Usuario registrado", + "failure": "No se pudo registrar al usuario" } } \ No newline at end of file diff --git a/src/services/platform/platform.ts b/src/services/platform/platform.ts index b92c79b..247e722 100644 --- a/src/services/platform/platform.ts +++ b/src/services/platform/platform.ts @@ -6,6 +6,7 @@ import { Logger } from "../logger/logger" import { UserInfo } from "./slack/methods/get-user-info" import { SlackBody } from "../../models/platform/slack/body" import { Community } from "../../models/database/community" +import { RegisterProps } from "../../actions/register/register"; export type PlatformName = "slack" @@ -59,4 +60,6 @@ export abstract class Platform { abstract getThanksProps: (data: any) => Promise abstract getInteractiveProps: (data: any) => Promise abstract getCoffeeRouletteProps: (data: any) => Promise + + abstract getRegisterProps: (data: any) => Promise } diff --git a/src/services/platform/slack/methods/get-user-info.ts b/src/services/platform/slack/methods/get-user-info.ts index dc7a6ce..46e8c95 100644 --- a/src/services/platform/slack/methods/get-user-info.ts +++ b/src/services/platform/slack/methods/get-user-info.ts @@ -22,12 +22,10 @@ export const getUserInfo = (request: Request, headers: any) => async ( }) Logger.onResponse(endpoint, { status, error: data.error }) - return data.ok - ? { - id: data.user.id, - name: data.user.name, - isBot: data.user.is_bot, - isAvailable: await getUserPresence(request, headers)(userId), - } - : undefined + return data.ok ? { + id: data.user.id, + name: data.user.real_name, + isBot: data.user.is_bot, + isAvailable: await getUserPresence(request, headers)(userId) + } : undefined } diff --git a/src/services/platform/slack/methods/get-user-presence.ts b/src/services/platform/slack/methods/get-user-presence.ts index 28d0024..457d9ec 100644 --- a/src/services/platform/slack/methods/get-user-presence.ts +++ b/src/services/platform/slack/methods/get-user-presence.ts @@ -11,5 +11,5 @@ export const getUserPresence = (request: Request, headers: any) => async (userId }) Logger.onResponse(endpoint, { status, error: data.error }) - return data.presence === "active" ? true : false + return data.presence === "active" } diff --git a/src/services/platform/slack/props/interactive-props.ts b/src/services/platform/slack/props/interactive-props.ts index d36bdf2..492e8f6 100644 --- a/src/services/platform/slack/props/interactive-props.ts +++ b/src/services/platform/slack/props/interactive-props.ts @@ -2,6 +2,7 @@ import { InteractiveProps } from "../../../../actions/interactive" import { SlackBody } from "../../../../models/platform/slack/body" import { getSlackButtonAction } from "./button-props" import { getSlackThanksConfirmationProps } from "./thanks-props" +import { getSlackRegisterProps } from "./register-props"; interface Action { getProps: (body: SlackBody) => any @@ -28,6 +29,10 @@ export const getSlackInteractiveProps = async (body: SlackBody): Promise => ({ + userId: body.user_id, + userName: body.user_name, +}) \ No newline at end of file diff --git a/src/services/platform/slack/slack.spec.ts b/src/services/platform/slack/slack.spec.ts index 0560000..b5cdfa0 100644 --- a/src/services/platform/slack/slack.spec.ts +++ b/src/services/platform/slack/slack.spec.ts @@ -207,7 +207,7 @@ describe("Slack service:", () => { const userId = "irrelevant-user-id" const userInfoJson = { id: userId, - name: "irrelevant-name", + real_name: "irrelevant-name", is_bot: false, } @@ -228,7 +228,7 @@ describe("Slack service:", () => { }) expect(userInfoReceived?.id).toBe(userInfoJson.id) - expect(userInfoReceived?.name).toBe(userInfoJson.name) + expect(userInfoReceived?.name).toBe(userInfoJson.real_name) expect(userInfoReceived?.isBot).toBe(userInfoJson.is_bot) }) diff --git a/src/services/platform/slack/slack.ts b/src/services/platform/slack/slack.ts index e6d2e03..06e4671 100644 --- a/src/services/platform/slack/slack.ts +++ b/src/services/platform/slack/slack.ts @@ -7,6 +7,7 @@ import { getConversationMembers, getTeamMembers, getUserInfo } from "./methods" import { getSlackCoffeeRouletteProps } from "./props/coffee-roulette-props" import { getSlackInteractiveProps } from "./props/interactive-props" import { getSlackThanksProps } from "./props/thanks-props" +import { getSlackRegisterProps } from "./props/register-props" import { CoffeeRouletteMessage, TryAgainCoffeeMessage } from "./views/coffee-roulette-views" import { GratitudeSummaryView } from "./views/view-gratitude-summary" import { GratitudeMessageInteractiveView } from "./views/view-gratitude-message" @@ -95,6 +96,7 @@ export class Slack extends Platform { getThanksProps = getSlackThanksProps getInteractiveProps = getSlackInteractiveProps getCoffeeRouletteProps = getSlackCoffeeRouletteProps + getRegisterProps = getSlackRegisterProps } Platform.dictionary["slack"] = Slack.getInstance() diff --git a/src/tests/builders/models/user-builder.ts b/src/tests/builders/models/user-builder.ts new file mode 100644 index 0000000..2a7d1e9 --- /dev/null +++ b/src/tests/builders/models/user-builder.ts @@ -0,0 +1,9 @@ +import { User } from "../../../models/database/user"; + +export const UserBuilder = ({ + userId = "irrelevant-user-id", + userName = "irrelevant-user-name" +}: Partial): User => new User( + userId, + userName +) \ No newline at end of file