From 671de082611b72eaeafb9cfa3f6da385ba76cfd6 Mon Sep 17 00:00:00 2001 From: Alfian Tirta Date: Fri, 20 Nov 2020 23:53:55 +0700 Subject: [PATCH 01/37] first commit --- db_schema.png | Bin 0 -> 46555 bytes server/.gitignore | 2 + server/services/users/package-lock.json | 1670 +++++++++++++++++ server/services/users/package.json | 25 + server/services/users/src/config/config.js | 25 + .../users/src/controllers/UserController.js | 18 + server/services/users/src/index.js | 35 + .../migrations/20201120154305-create-user.js | 36 + .../migrations/20201120154501-create-post.js | 30 + .../migrations/20201120155020-create-like.js | 27 + .../20201120155127-create-comment.js | 27 + .../20201120155140-create-sub-comment.js | 27 + server/services/users/src/models/comment.js | 23 + server/services/users/src/models/index.js | 37 + server/services/users/src/models/like.js | 23 + server/services/users/src/models/post.js | 24 + .../services/users/src/models/subcomment.js | 23 + server/services/users/src/models/user.js | 26 + server/services/users/src/routes/index.js | 7 + .../users/src/routes/register/index.js | 8 + 20 files changed, 2093 insertions(+) create mode 100644 db_schema.png create mode 100644 server/.gitignore create mode 100644 server/services/users/package-lock.json create mode 100644 server/services/users/package.json create mode 100644 server/services/users/src/config/config.js create mode 100644 server/services/users/src/controllers/UserController.js create mode 100644 server/services/users/src/index.js create mode 100644 server/services/users/src/migrations/20201120154305-create-user.js create mode 100644 server/services/users/src/migrations/20201120154501-create-post.js create mode 100644 server/services/users/src/migrations/20201120155020-create-like.js create mode 100644 server/services/users/src/migrations/20201120155127-create-comment.js create mode 100644 server/services/users/src/migrations/20201120155140-create-sub-comment.js create mode 100644 server/services/users/src/models/comment.js create mode 100644 server/services/users/src/models/index.js create mode 100644 server/services/users/src/models/like.js create mode 100644 server/services/users/src/models/post.js create mode 100644 server/services/users/src/models/subcomment.js create mode 100644 server/services/users/src/models/user.js create mode 100644 server/services/users/src/routes/index.js create mode 100644 server/services/users/src/routes/register/index.js diff --git a/db_schema.png b/db_schema.png new file mode 100644 index 0000000000000000000000000000000000000000..421be55a086ca4177436915193e82bd0b282991b GIT binary patch literal 46555 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 G^Zx-(sVs8< literal 0 HcmV?d00001 diff --git a/server/.gitignore b/server/.gitignore new file mode 100644 index 0000000..790c219 --- /dev/null +++ b/server/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +.env* diff --git a/server/services/users/package-lock.json b/server/services/users/package-lock.json new file mode 100644 index 0000000..041f10d --- /dev/null +++ b/server/services/users/package-lock.json @@ -0,0 +1,1670 @@ +{ + "name": "users", + "version": "1.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@sindresorhus/is": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz", + "integrity": "sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ==", + "dev": true + }, + "@szmarczak/http-timer": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-1.1.2.tgz", + "integrity": "sha512-XIB2XbzHTN6ieIjfIMV9hlVcfPU26s2vafYWQcZHWXHOxiaRZYEDKEwdl129Zyg50+foYV2jCgtrqSA6qNuNSA==", + "dev": true, + "requires": { + "defer-to-connect": "^1.0.1" + } + }, + "@types/node": { + "version": "14.14.9", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.9.tgz", + "integrity": "sha512-JsoLXFppG62tWTklIoO4knA+oDTYsmqWxHRvd4lpmfQRNhX6osheUOWETP2jMoV/2bEHuMra8Pp3Dmo/stBFcw==" + }, + "abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "dev": true + }, + "accepts": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", + "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==", + "requires": { + "mime-types": "~2.1.24", + "negotiator": "0.6.2" + } + }, + "ansi-align": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.0.tgz", + "integrity": "sha512-ZpClVKqXN3RGBmKibdfWzqCY4lnjEuoNzU5T0oEFpfd/z5qJHVarukridD4juLO2FXMiwUQxr9WqQtaYa8XRYw==", + "dev": true, + "requires": { + "string-width": "^3.0.0" + }, + "dependencies": { + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + } + } + }, + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha1-q8av7tzqUugJzcA3au0845Y10X8=" + }, + "anymatch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz", + "integrity": "sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==", + "dev": true, + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true + }, + "binary-extensions": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.1.0.tgz", + "integrity": "sha512-1Yj8h9Q+QDF5FzhMs/c9+6UntbD5MkRfRwac8DoEm9ZfUBZ7tZ55YcGVAzEe4bXsdQHEk+s9S5wsOKVdZrw0tQ==", + "dev": true + }, + "boxen": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/boxen/-/boxen-4.2.0.tgz", + "integrity": "sha512-eB4uT9RGzg2odpER62bBwSLvUeGC+WbRjjyyFhGsKnc8wp/m0+hQsMUvUe3H2V0D5vw0nBdO1hCJoZo5mKeuIQ==", + "dev": true, + "requires": { + "ansi-align": "^3.0.0", + "camelcase": "^5.3.1", + "chalk": "^3.0.0", + "cli-boxes": "^2.2.0", + "string-width": "^4.1.0", + "term-size": "^2.1.0", + "type-fest": "^0.8.1", + "widest-line": "^3.1.0" + } + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "buffer-writer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/buffer-writer/-/buffer-writer-2.0.0.tgz", + "integrity": "sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw==" + }, + "bytes": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", + "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==" + }, + "cache-content-type": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cache-content-type/-/cache-content-type-1.0.1.tgz", + "integrity": "sha512-IKufZ1o4Ut42YUrZSo8+qnMTrFuKkvyoLXUywKz9GJ5BrhOFGhLdkx9sG4KAnVvbY6kEcSFjLQul+DVmBm2bgA==", + "requires": { + "mime-types": "^2.1.18", + "ylru": "^1.2.0" + } + }, + "cacheable-request": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-6.1.0.tgz", + "integrity": "sha512-Oj3cAGPCqOZX7Rz64Uny2GYAZNliQSqfbePrgAQ1wKAihYmCUnraBtJtKcGR4xz7wF+LoJC+ssFZvv5BgF9Igg==", + "dev": true, + "requires": { + "clone-response": "^1.0.2", + "get-stream": "^5.1.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^3.0.0", + "lowercase-keys": "^2.0.0", + "normalize-url": "^4.1.0", + "responselike": "^1.0.2" + }, + "dependencies": { + "get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "requires": { + "pump": "^3.0.0" + } + }, + "lowercase-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", + "dev": true + } + } + }, + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true + }, + "chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "dependencies": { + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "chokidar": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.4.3.tgz", + "integrity": "sha512-DtM3g7juCXQxFVSNPNByEC2+NImtBuxQQvWlHunpJIS5Ocr0lG306cC7FCi7cEA0fzmybPUIl4txBIobk1gGOQ==", + "dev": true, + "requires": { + "anymatch": "~3.1.1", + "braces": "~3.0.2", + "fsevents": "~2.1.2", + "glob-parent": "~5.1.0", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.5.0" + } + }, + "ci-info": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", + "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", + "dev": true + }, + "cli-boxes": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-2.2.1.tgz", + "integrity": "sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw==", + "dev": true + }, + "clone-response": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz", + "integrity": "sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws=", + "dev": true, + "requires": { + "mimic-response": "^1.0.0" + } + }, + "co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=" + }, + "co-body": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/co-body/-/co-body-6.1.0.tgz", + "integrity": "sha512-m7pOT6CdLN7FuXUcpuz/8lfQ/L77x8SchHCF4G0RBTJO20Wzmhn5Sp4/5WsKy8OSpifBSUrmg83qEqaDHdyFuQ==", + "requires": { + "inflation": "^2.0.0", + "qs": "^6.5.2", + "raw-body": "^2.3.3", + "type-is": "^1.6.16" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "configstore": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/configstore/-/configstore-5.0.1.tgz", + "integrity": "sha512-aMKprgk5YhBNyH25hj8wGt2+D52Sw1DRRIzqBwLp2Ya9mFmY8KPvvtvmna8SxVR9JMZ4kzMD68N22vlaRpkeFA==", + "dev": true, + "requires": { + "dot-prop": "^5.2.0", + "graceful-fs": "^4.1.2", + "make-dir": "^3.0.0", + "unique-string": "^2.0.0", + "write-file-atomic": "^3.0.0", + "xdg-basedir": "^4.0.0" + } + }, + "content-disposition": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", + "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==", + "requires": { + "safe-buffer": "5.1.2" + } + }, + "content-type": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" + }, + "cookies": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/cookies/-/cookies-0.8.0.tgz", + "integrity": "sha512-8aPsApQfebXnuI+537McwYsDtjVxGm8gTIzQI3FDW6t5t/DAhERxtnbEPN/8RX+uZthoz4eCOgloXaE5cYyNow==", + "requires": { + "depd": "~2.0.0", + "keygrip": "~1.1.0" + }, + "dependencies": { + "depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" + } + } + }, + "copy-to": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/copy-to/-/copy-to-2.0.1.tgz", + "integrity": "sha1-JoD7uAaKSNCGVrYJgJK9r8kG9KU=" + }, + "crypto-random-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", + "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==", + "dev": true + }, + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "requires": { + "ms": "2.0.0" + } + }, + "decompress-response": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-3.3.0.tgz", + "integrity": "sha1-gKTdMjdIOEv6JICDYirt7Jgq3/M=", + "dev": true, + "requires": { + "mimic-response": "^1.0.0" + } + }, + "deep-equal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz", + "integrity": "sha1-9dJgKStmDghO/0zbyfCK0yR0SLU=" + }, + "deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "dev": true + }, + "defer-to-connect": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-1.1.3.tgz", + "integrity": "sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ==", + "dev": true + }, + "delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=" + }, + "depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" + }, + "destroy": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", + "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" + }, + "dot-prop": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", + "integrity": "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==", + "dev": true, + "requires": { + "is-obj": "^2.0.0" + } + }, + "dotenv": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.2.0.tgz", + "integrity": "sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw==" + }, + "dottie": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/dottie/-/dottie-2.0.2.tgz", + "integrity": "sha512-fmrwR04lsniq/uSr8yikThDTrM7epXHBAAjH9TbeH3rEA8tdCO7mRzB9hdmdGyJCxF8KERo9CITcm3kGuoyMhg==" + }, + "duplexer3": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz", + "integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=", + "dev": true + }, + "ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" + }, + "emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "dev": true + }, + "encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" + }, + "end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dev": true, + "requires": { + "once": "^1.4.0" + } + }, + "escape-goat": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/escape-goat/-/escape-goat-2.1.1.tgz", + "integrity": "sha512-8/uIhbG12Csjy2JEW7D9pHbreaVaS/OpN3ycnyvElTdwM5n6GY6W6e2IPemfvGZeUMqZ9A/3GqIZMgKnBhAw/Q==", + "dev": true + }, + "escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" + }, + "fsevents": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.3.tgz", + "integrity": "sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ==", + "dev": true, + "optional": true + }, + "get-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "dev": true, + "requires": { + "pump": "^3.0.0" + } + }, + "glob-parent": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz", + "integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + }, + "global-dirs": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-2.0.1.tgz", + "integrity": "sha512-5HqUqdhkEovj2Of/ms3IeS/EekcO54ytHRLV4PEY2rhRwrHXLQjeVEES0Lhka0xwNDtGYn58wyC4s5+MHsOO6A==", + "dev": true, + "requires": { + "ini": "^1.3.5" + } + }, + "got": { + "version": "9.6.0", + "resolved": "https://registry.npmjs.org/got/-/got-9.6.0.tgz", + "integrity": "sha512-R7eWptXuGYxwijs0eV+v3o6+XH1IqVK8dJOEecQfTmkncw9AV4dcw/Dhxi8MdlqPthxxpZyizMzyg8RTmEsG+Q==", + "dev": true, + "requires": { + "@sindresorhus/is": "^0.14.0", + "@szmarczak/http-timer": "^1.1.2", + "cacheable-request": "^6.0.0", + "decompress-response": "^3.3.0", + "duplexer3": "^0.1.4", + "get-stream": "^4.1.0", + "lowercase-keys": "^1.0.1", + "mimic-response": "^1.0.1", + "p-cancelable": "^1.0.0", + "to-readable-stream": "^1.0.0", + "url-parse-lax": "^3.0.0" + } + }, + "graceful-fs": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", + "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==", + "dev": true + }, + "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-yarn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/has-yarn/-/has-yarn-2.1.0.tgz", + "integrity": "sha512-UqBRqi4ju7T+TqGNdqAO0PaSVGsDGJUBQvk9eUWNGRY1CFGDzYhLWoM7JQEemnlvVcv/YEmc2wNW8BC24EnUsw==", + "dev": true + }, + "http-assert": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/http-assert/-/http-assert-1.4.1.tgz", + "integrity": "sha512-rdw7q6GTlibqVVbXr0CKelfV5iY8G2HqEUkhSk297BMbSpSL8crXC+9rjKoMcZZEsksX30le6f/4ul4E28gegw==", + "requires": { + "deep-equal": "~1.0.1", + "http-errors": "~1.7.2" + }, + "dependencies": { + "http-errors": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.3.tgz", + "integrity": "sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw==", + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.4", + "setprototypeof": "1.1.1", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" + } + } + } + }, + "http-cache-semantics": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz", + "integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==", + "dev": true + }, + "http-errors": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.0.tgz", + "integrity": "sha512-4I8r0C5JDhT5VkvI47QktDW75rNlGVsUf/8hzjCC/wkWI/jdTRmBb9aI7erSG82r1bjKY3F6k28WnsVxB1C73A==", + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" + }, + "dependencies": { + "setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + } + } + }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha1-SMptcvbGo68Aqa1K5odr44ieKwk=", + "dev": true + }, + "import-lazy": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-2.1.0.tgz", + "integrity": "sha1-BWmOPUXIjo1+nZLLBYTnfwlvPkM=", + "dev": true + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "dev": true + }, + "inflation": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/inflation/-/inflation-2.0.0.tgz", + "integrity": "sha1-i0F+R8KPklpFEz2RTKH9OJEH8w8=" + }, + "inflection": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/inflection/-/inflection-1.12.0.tgz", + "integrity": "sha1-ogCTVlbW9fa8TcdQLhrstwMihBY=" + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "ini": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", + "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==", + "dev": true + }, + "is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "requires": { + "binary-extensions": "^2.0.0" + } + }, + "is-ci": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-2.0.0.tgz", + "integrity": "sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==", + "dev": true, + "requires": { + "ci-info": "^2.0.0" + } + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "is-generator-function": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.7.tgz", + "integrity": "sha512-YZc5EwyO4f2kWCax7oegfuSr9mFz1ZvieNYBEjmukLxgXfBUbxAWGVF7GZf0zidYtoBl3WvC07YK0wT76a+Rtw==" + }, + "is-glob": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", + "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-installed-globally": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.3.2.tgz", + "integrity": "sha512-wZ8x1js7Ia0kecP/CHM/3ABkAmujX7WPvQk6uu3Fly/Mk44pySulQpnHG46OMjHGXApINnV4QhY3SWnECO2z5g==", + "dev": true, + "requires": { + "global-dirs": "^2.0.1", + "is-path-inside": "^3.0.1" + } + }, + "is-npm": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-4.0.0.tgz", + "integrity": "sha512-96ECIfh9xtDDlPylNPXhzjsykHsMJZ18ASpaWzQyBr4YRTcVjUvzaHayDAES2oU/3KpljhHUjtSRNiDwi0F0ig==", + "dev": true + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "is-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", + "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", + "dev": true + }, + "is-path-inside": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.2.tgz", + "integrity": "sha512-/2UGPSgmtqwo1ktx8NDHjuPwZWmHhO+gj0f93EkhLB5RgW9RZevWYYlIkS6zePc6U2WpOdQYIwHe9YC4DWEBVg==", + "dev": true + }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", + "dev": true + }, + "is-yarn-global": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/is-yarn-global/-/is-yarn-global-0.3.0.tgz", + "integrity": "sha512-VjSeb/lHmkoyd8ryPVIKvOCn4D1koMqY+vqyjjUfc3xyKtP4dYOxM44sZrnqQSzSds3xyOrUTLTC9LVCVgLngw==", + "dev": true + }, + "json-buffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.0.tgz", + "integrity": "sha1-Wx85evx11ne96Lz8Dkfh+aPZqJg=", + "dev": true + }, + "kcors": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/kcors/-/kcors-2.2.2.tgz", + "integrity": "sha512-rIqbKa2S0gT0wC/790jsQM6hNpABHBNWQ7+XYS1xJV6zOGxlanW+RtCmlDn6wPZsGpRk371yy8abfBgl2OTavg==" + }, + "keygrip": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/keygrip/-/keygrip-1.1.0.tgz", + "integrity": "sha512-iYSchDJ+liQ8iwbSI2QqsQOvqv58eJCEanyJPJi+Khyu8smkcKSFUCbPwzFcL7YVtZ6eONjqRX/38caJ7QjRAQ==", + "requires": { + "tsscmp": "1.0.6" + } + }, + "keyv": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-3.1.0.tgz", + "integrity": "sha512-9ykJ/46SN/9KPM/sichzQ7OvXyGDYKGTaDlKMGCAlg2UK8KRy4jb0d8sFc+0Tt0YYnThq8X2RZgCg74RPxgcVA==", + "dev": true, + "requires": { + "json-buffer": "3.0.0" + } + }, + "koa": { + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/koa/-/koa-2.13.0.tgz", + "integrity": "sha512-i/XJVOfPw7npbMv67+bOeXr3gPqOAw6uh5wFyNs3QvJ47tUx3M3V9rIE0//WytY42MKz4l/MXKyGkQ2LQTfLUQ==", + "requires": { + "accepts": "^1.3.5", + "cache-content-type": "^1.0.0", + "content-disposition": "~0.5.2", + "content-type": "^1.0.4", + "cookies": "~0.8.0", + "debug": "~3.1.0", + "delegates": "^1.0.0", + "depd": "^1.1.2", + "destroy": "^1.0.4", + "encodeurl": "^1.0.2", + "escape-html": "^1.0.3", + "fresh": "~0.5.2", + "http-assert": "^1.3.0", + "http-errors": "^1.6.3", + "is-generator-function": "^1.0.7", + "koa-compose": "^4.1.0", + "koa-convert": "^1.2.0", + "on-finished": "^2.3.0", + "only": "~0.0.2", + "parseurl": "^1.3.2", + "statuses": "^1.5.0", + "type-is": "^1.6.16", + "vary": "^1.1.2" + } + }, + "koa-bodyparser": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/koa-bodyparser/-/koa-bodyparser-4.3.0.tgz", + "integrity": "sha512-uyV8G29KAGwZc4q/0WUAjH+Tsmuv9ImfBUF2oZVyZtaeo0husInagyn/JH85xMSxM0hEk/mbCII5ubLDuqW/Rw==", + "requires": { + "co-body": "^6.0.0", + "copy-to": "^2.0.1" + } + }, + "koa-compose": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/koa-compose/-/koa-compose-4.1.0.tgz", + "integrity": "sha512-8ODW8TrDuMYvXRwra/Kh7/rJo9BtOfPc6qO8eAfC80CnCvSjSl0bkRM24X6/XBBEyj0v1nRUQ1LyOy3dbqOWXw==" + }, + "koa-convert": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/koa-convert/-/koa-convert-1.2.0.tgz", + "integrity": "sha1-2kCHXfSd4FOQmNFwC1CCDOvNIdA=", + "requires": { + "co": "^4.6.0", + "koa-compose": "^3.0.0" + }, + "dependencies": { + "koa-compose": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/koa-compose/-/koa-compose-3.2.1.tgz", + "integrity": "sha1-qFzLQLfZhtjlo0Wzoazo6rz1Tec=", + "requires": { + "any-promise": "^1.1.0" + } + } + } + }, + "koa-router": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/koa-router/-/koa-router-10.0.0.tgz", + "integrity": "sha512-gAE5J1gBQTvfR8rMMtMUkE26+1MbO3DGpGmvfmM2pR9Z7w2VIb2Ecqeal98yVO7+4ltffby7gWOzpCmdNOQe0w==", + "requires": { + "debug": "^4.1.1", + "http-errors": "^1.7.3", + "koa-compose": "^4.1.0", + "methods": "^1.1.2", + "path-to-regexp": "^6.1.0" + }, + "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==" + } + } + }, + "latest-version": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-5.1.0.tgz", + "integrity": "sha512-weT+r0kTkRQdCdYCNtkMwWXQTMEswKrFBkm4ckQOMVhhqhIMI1UT2hMj+1iigIhgSZm5gTmrRXBNoGUgaTY1xA==", + "dev": true, + "requires": { + "package-json": "^6.3.0" + } + }, + "lodash": { + "version": "4.17.20", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", + "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==" + }, + "lowercase-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz", + "integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==", + "dev": true + }, + "make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "requires": { + "semver": "^6.0.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, + "media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" + }, + "methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" + }, + "mime-db": { + "version": "1.44.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.44.0.tgz", + "integrity": "sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg==" + }, + "mime-types": { + "version": "2.1.27", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.27.tgz", + "integrity": "sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w==", + "requires": { + "mime-db": "1.44.0" + } + }, + "mimic-response": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", + "dev": true + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", + "dev": true + }, + "moment": { + "version": "2.29.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz", + "integrity": "sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ==" + }, + "moment-timezone": { + "version": "0.5.32", + "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.32.tgz", + "integrity": "sha512-Z8QNyuQHQAmWucp8Knmgei8YNo28aLjJq6Ma+jy1ZSpSk5nyfRT8xgUbSQvD2+2UajISfenndwvFuH3NGS+nvA==", + "requires": { + "moment": ">= 2.9.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "negotiator": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", + "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==" + }, + "nodemon": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.6.tgz", + "integrity": "sha512-4I3YDSKXg6ltYpcnZeHompqac4E6JeAMpGm8tJnB9Y3T0ehasLa4139dJOcCrB93HHrUMsCrKtoAlXTqT5n4AQ==", + "dev": true, + "requires": { + "chokidar": "^3.2.2", + "debug": "^3.2.6", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.0.4", + "pstree.remy": "^1.1.7", + "semver": "^5.7.1", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.3", + "update-notifier": "^4.1.0" + }, + "dependencies": { + "debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + } + } + }, + "nopt": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz", + "integrity": "sha1-bd0hvSoxQXuScn3Vhfim83YI6+4=", + "dev": true, + "requires": { + "abbrev": "1" + } + }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true + }, + "normalize-url": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-4.5.0.tgz", + "integrity": "sha512-2s47yzUxdexf1OhyRi4Em83iQk0aPvwTddtFz4hnSSw9dCEsLEGf6SwIO8ss/19S9iBb5sJaOuTvTGDeZI00BQ==", + "dev": true + }, + "on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", + "requires": { + "ee-first": "1.1.1" + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "only": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/only/-/only-0.0.2.tgz", + "integrity": "sha1-Kv3oTQPlC5qO3EROMGEKcCle37Q=" + }, + "p-cancelable": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-1.1.0.tgz", + "integrity": "sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw==", + "dev": true + }, + "package-json": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/package-json/-/package-json-6.5.0.tgz", + "integrity": "sha512-k3bdm2n25tkyxcjSKzB5x8kfVxlMdgsbPr0GkZcwHsLpba6cBjqCt1KlcChKEvxHIcTB1FVMuwoijZ26xex5MQ==", + "dev": true, + "requires": { + "got": "^9.6.0", + "registry-auth-token": "^4.0.0", + "registry-url": "^5.0.0", + "semver": "^6.2.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, + "packet-reader": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/packet-reader/-/packet-reader-1.0.0.tgz", + "integrity": "sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ==" + }, + "parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" + }, + "path-to-regexp": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.2.0.tgz", + "integrity": "sha512-f66KywYG6+43afgE/8j/GoiNyygk/bnoCbps++3ErRKsIYkGGupyv07R2Ok5m9i67Iqc+T2g1eAUGUPzWhYTyg==" + }, + "pg": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.5.1.tgz", + "integrity": "sha512-9wm3yX9lCfjvA98ybCyw2pADUivyNWT/yIP4ZcDVpMN0og70BUWYEGXPCTAQdGTAqnytfRADb7NERrY1qxhIqw==", + "requires": { + "buffer-writer": "2.0.0", + "packet-reader": "1.0.0", + "pg-connection-string": "^2.4.0", + "pg-pool": "^3.2.2", + "pg-protocol": "^1.4.0", + "pg-types": "^2.1.0", + "pgpass": "1.x" + } + }, + "pg-connection-string": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.4.0.tgz", + "integrity": "sha512-3iBXuv7XKvxeMrIgym7njT+HlZkwZqqGX4Bu9cci8xHZNT+Um1gWKqCsAzcC0d95rcKMU5WBg6YRUcHyV0HZKQ==" + }, + "pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==" + }, + "pg-pool": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.2.2.tgz", + "integrity": "sha512-ORJoFxAlmmros8igi608iVEbQNNZlp89diFVx6yV5v+ehmpMY9sK6QgpmgoXbmkNaBAx8cOOZh9g80kJv1ooyA==" + }, + "pg-protocol": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.4.0.tgz", + "integrity": "sha512-El+aXWcwG/8wuFICMQjM5ZSAm6OWiJicFdNYo+VY3QP+8vI4SvLIWVe51PppTzMhikUJR+PsyIFKqfdXPz/yxA==" + }, + "pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "requires": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + } + }, + "pgpass": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.4.tgz", + "integrity": "sha512-YmuA56alyBq7M59vxVBfPJrGSozru8QAdoNlWuW3cz8l+UX3cWge0vTvjKhsSHSJpo3Bom8/Mm6hf0TR5GY0+w==", + "requires": { + "split2": "^3.1.1" + } + }, + "picomatch": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz", + "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==", + "dev": true + }, + "postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==" + }, + "postgres-bytea": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz", + "integrity": "sha1-AntTPAqokOJtFy1Hz5zOzFIazTU=" + }, + "postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==" + }, + "postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "requires": { + "xtend": "^4.0.0" + } + }, + "prepend-http": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz", + "integrity": "sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=", + "dev": true + }, + "pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true + }, + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dev": true, + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "pupa": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/pupa/-/pupa-2.1.1.tgz", + "integrity": "sha512-l1jNAspIBSFqbT+y+5FosojNpVpF94nlI+wDUpqP9enwOTfHx9f0gh5nB96vl+6yTpsJsypeNrwfzPrKuHB41A==", + "dev": true, + "requires": { + "escape-goat": "^2.0.0" + } + }, + "qs": { + "version": "6.9.4", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.9.4.tgz", + "integrity": "sha512-A1kFqHekCTM7cz0udomYUoYNWjBebHm/5wzU/XqrBRBNWectVH0QIiN+NEcZ0Dte5hvzHwbr8+XQmguPhJ6WdQ==" + }, + "raw-body": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.1.tgz", + "integrity": "sha512-9WmIKF6mkvA0SLmA2Knm9+qj89e+j1zqgyn8aXGd7+nAduPoqgI9lO57SAZNn/Byzo5P7JhXTyg9PzaJbH73bA==", + "requires": { + "bytes": "3.1.0", + "http-errors": "1.7.3", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "dependencies": { + "http-errors": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.3.tgz", + "integrity": "sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw==", + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.4", + "setprototypeof": "1.1.1", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" + } + } + } + }, + "rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "dev": true, + "requires": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + } + }, + "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" + } + }, + "readdirp": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.5.0.tgz", + "integrity": "sha512-cMhu7c/8rdhkHXWsY+osBhfSy0JikwpHK/5+imo+LpeasTF8ouErHrlYkwT0++njiyuDvc7OFY5T3ukvZ8qmFQ==", + "dev": true, + "requires": { + "picomatch": "^2.2.1" + } + }, + "registry-auth-token": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-4.2.1.tgz", + "integrity": "sha512-6gkSb4U6aWJB4SF2ZvLb76yCBjcvufXBqvvEx1HbmKPkutswjW1xNVRY0+daljIYRbogN7O0etYSlbiaEQyMyw==", + "dev": true, + "requires": { + "rc": "^1.2.8" + } + }, + "registry-url": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-5.1.0.tgz", + "integrity": "sha512-8acYXXTI0AkQv6RAOjE3vOaIXZkT9wo4LOFbBKYQEEnnMNBpKqdUrI6S4NT0KPIo/WVvJ5tE/X5LF/TQUf0ekw==", + "dev": true, + "requires": { + "rc": "^1.2.8" + } + }, + "responselike": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-1.0.2.tgz", + "integrity": "sha1-kYcg7ztjHFZCvgaPFa3lpG9Loec=", + "dev": true, + "requires": { + "lowercase-keys": "^1.0.0" + } + }, + "retry-as-promised": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/retry-as-promised/-/retry-as-promised-3.2.0.tgz", + "integrity": "sha512-CybGs60B7oYU/qSQ6kuaFmRd9sTZ6oXSc0toqePvV74Ac6/IFZSI1ReFQmtCN+uvW1Mtqdwpvt/LGOiCBAY2Mg==", + "requires": { + "any-promise": "^1.3.0" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "semver": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz", + "integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==" + }, + "semver-diff": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/semver-diff/-/semver-diff-3.1.1.tgz", + "integrity": "sha512-GX0Ix/CJcHyB8c4ykpHGIAvLyOwOobtM/8d+TQkAd81/bEjgPHrfba41Vpesr7jX/t8Uh+R3EX9eAS5be+jQYg==", + "dev": true, + "requires": { + "semver": "^6.3.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, + "sequelize": { + "version": "6.3.5", + "resolved": "https://registry.npmjs.org/sequelize/-/sequelize-6.3.5.tgz", + "integrity": "sha512-MiwiPkYSA8NWttRKAXdU9h0TxP6HAc1fl7qZmMO/VQqQOND83G4nZLXd0kWILtAoT9cxtZgFqeb/MPYgEeXwsw==", + "requires": { + "debug": "^4.1.1", + "dottie": "^2.0.0", + "inflection": "1.12.0", + "lodash": "^4.17.15", + "moment": "^2.26.0", + "moment-timezone": "^0.5.31", + "retry-as-promised": "^3.2.0", + "semver": "^7.3.2", + "sequelize-pool": "^6.0.0", + "toposort-class": "^1.0.1", + "uuid": "^8.1.0", + "validator": "^10.11.0", + "wkx": "^0.5.0" + }, + "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==" + } + } + }, + "sequelize-pool": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/sequelize-pool/-/sequelize-pool-6.1.0.tgz", + "integrity": "sha512-4YwEw3ZgK/tY/so+GfnSgXkdwIJJ1I32uZJztIEgZeAO6HMgj64OzySbWLgxj+tXhZCJnzRfkY9gINw8Ft8ZMg==" + }, + "setprototypeof": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", + "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" + }, + "signal-exit": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", + "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==", + "dev": true + }, + "split2": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/split2/-/split2-3.2.2.tgz", + "integrity": "sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg==", + "requires": { + "readable-stream": "^3.0.0" + } + }, + "statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" + }, + "string-width": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", + "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "dev": true + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true + }, + "strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.0" + } + } + } + }, + "string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "requires": { + "safe-buffer": "~5.2.0" + }, + "dependencies": { + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + } + } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + }, + "strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + }, + "term-size": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/term-size/-/term-size-2.2.1.tgz", + "integrity": "sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==", + "dev": true + }, + "to-readable-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/to-readable-stream/-/to-readable-stream-1.0.0.tgz", + "integrity": "sha512-Iq25XBt6zD5npPhlLVXGFN3/gyR2/qODcKNNyTMd4vbm39HUaOiAM4PMq0eMVC/Tkxz+Zjdsc55g9yyz+Yq00Q==", + "dev": true + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + }, + "toidentifier": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", + "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==" + }, + "toposort-class": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toposort-class/-/toposort-class-1.0.1.tgz", + "integrity": "sha1-f/0feMi+KMO6Rc1OGj9e4ZO9mYg=" + }, + "touch": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz", + "integrity": "sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==", + "dev": true, + "requires": { + "nopt": "~1.0.10" + } + }, + "tsscmp": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.6.tgz", + "integrity": "sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==" + }, + "type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true + }, + "type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "requires": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + } + }, + "typedarray-to-buffer": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", + "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", + "dev": true, + "requires": { + "is-typedarray": "^1.0.0" + } + }, + "undefsafe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.3.tgz", + "integrity": "sha512-nrXZwwXrD/T/JXeygJqdCO6NZZ1L66HrxM/Z7mIq2oPanoN0F1nLx3lwJMu6AwJY69hdixaFQOuoYsMjE5/C2A==", + "dev": true, + "requires": { + "debug": "^2.2.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + } + } + }, + "unique-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz", + "integrity": "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==", + "dev": true, + "requires": { + "crypto-random-string": "^2.0.0" + } + }, + "unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" + }, + "update-notifier": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-4.1.3.tgz", + "integrity": "sha512-Yld6Z0RyCYGB6ckIjffGOSOmHXj1gMeE7aROz4MG+XMkmixBX4jUngrGXNYz7wPKBmtoD4MnBa2Anu7RSKht/A==", + "dev": true, + "requires": { + "boxen": "^4.2.0", + "chalk": "^3.0.0", + "configstore": "^5.0.1", + "has-yarn": "^2.1.0", + "import-lazy": "^2.1.0", + "is-ci": "^2.0.0", + "is-installed-globally": "^0.3.1", + "is-npm": "^4.0.0", + "is-yarn-global": "^0.3.0", + "latest-version": "^5.0.0", + "pupa": "^2.0.1", + "semver-diff": "^3.1.1", + "xdg-basedir": "^4.0.0" + } + }, + "url-parse-lax": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-3.0.0.tgz", + "integrity": "sha1-FrXK/Afb42dsGxmZF3gj1lA6yww=", + "dev": true, + "requires": { + "prepend-http": "^2.0.0" + } + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + }, + "uuid": { + "version": "8.3.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.1.tgz", + "integrity": "sha512-FOmRr+FmWEIG8uhZv6C2bTgEVXsHk08kE7mPlrBbEe+c3r9pjceVPgupIfNIhc4yx55H69OXANrUaSuu9eInKg==" + }, + "validator": { + "version": "10.11.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-10.11.0.tgz", + "integrity": "sha512-X/p3UZerAIsbBfN/IwahhYaBbY68EN/UQBWHtsbXGT5bfrH/p4NQzUCG1kF/rtKaNpnJ7jAu6NGTdSNtyNIXMw==" + }, + "vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" + }, + "widest-line": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-3.1.0.tgz", + "integrity": "sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==", + "dev": true, + "requires": { + "string-width": "^4.0.0" + } + }, + "wkx": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/wkx/-/wkx-0.5.0.tgz", + "integrity": "sha512-Xng/d4Ichh8uN4l0FToV/258EjMGU9MGcA0HV2d9B/ZpZB3lqQm7nkOdZdm5GhKtLLhAE7PiVQwN4eN+2YJJUg==", + "requires": { + "@types/node": "*" + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + }, + "write-file-atomic": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", + "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", + "dev": true, + "requires": { + "imurmurhash": "^0.1.4", + "is-typedarray": "^1.0.0", + "signal-exit": "^3.0.2", + "typedarray-to-buffer": "^3.1.5" + } + }, + "xdg-basedir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-4.0.0.tgz", + "integrity": "sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==", + "dev": true + }, + "xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==" + }, + "ylru": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ylru/-/ylru-1.2.1.tgz", + "integrity": "sha512-faQrqNMzcPCHGVC2aaOINk13K+aaBDUPjGWl0teOXywElLjyVAB6Oe2jj62jHYtwsU49jXhScYbvPENK+6zAvQ==" + } + } +} diff --git a/server/services/users/package.json b/server/services/users/package.json new file mode 100644 index 0000000..80f5d4e --- /dev/null +++ b/server/services/users/package.json @@ -0,0 +1,25 @@ +{ + "name": "users", + "version": "1.0.0", + "description": "", + "main": "index.js", + "dependencies": { + "dotenv": "^8.2.0", + "kcors": "^2.2.2", + "koa": "^2.13.0", + "koa-bodyparser": "^4.3.0", + "koa-router": "^10.0.0", + "pg": "^8.5.1", + "sequelize": "^6.3.5" + }, + "devDependencies": { + "nodemon": "^2.0.6" + }, + "scripts": { + "start": "nodemon src", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC" +} diff --git a/server/services/users/src/config/config.js b/server/services/users/src/config/config.js new file mode 100644 index 0000000..598eb28 --- /dev/null +++ b/server/services/users/src/config/config.js @@ -0,0 +1,25 @@ +require('dotenv').config(); + +module.exports = { + "development": { + "username": process.env.DB_USERNAME, + "password": process.env.DB_PASSWORD, + "database": process.env.DB_NAME, + "host": process.env.DB_HOST, + "dialect": process.env.DB_DIALECT, + }, + "test": { + "username": "root", + "password": null, + "database": "database_test", + "host": "127.0.0.1", + "dialect": "mysql", + }, + "production": { + "username": "root", + "password": null, + "database": "database_production", + "host": "127.0.0.1", + "dialect": "mysql", + } +} diff --git a/server/services/users/src/controllers/UserController.js b/server/services/users/src/controllers/UserController.js new file mode 100644 index 0000000..07b1eb8 --- /dev/null +++ b/server/services/users/src/controllers/UserController.js @@ -0,0 +1,18 @@ +class UserController { + static async register(ctx) { + // const { username, email, password } = ctx.request.body; + // try { + // const new_user = await User.create({ username, email, password }); + // return ctx.body = { + // id: new_user.id, + // email: new_user.email, + // }; + // } catch(err) { + // console.log(err); + // } + } + + static async login(ctx) {} +} + +module.exports = UserController; diff --git a/server/services/users/src/index.js b/server/services/users/src/index.js new file mode 100644 index 0000000..a6e6a58 --- /dev/null +++ b/server/services/users/src/index.js @@ -0,0 +1,35 @@ +'use strict'; + +require('dotenv').config(); +const cors = require('kcors'); +const Koa = require('koa'); +const Router = require('koa-router'); +const bodyParser = require('koa-bodyparser'); +const loadRoutes = require('./routes'); +const app = new Koa(); +const router = new Router(); +const port = process.env.PORT || 3000; + +app + .use(cors()) + .use(bodyParser()) + .use(router.routes()) + .use(router.allowedMethods()); + +loadRoutes(router); + +// router +// .get('/', ctx => { +// ctx.body = 'Hello World'; +// }) +// .get('/aeiou', ctx => { +// ctx.body = 'aeiou'; +// }); +// +// router.get('/test', ctx => { +// ctx.body = 'test'; +// }); + +app.listen(port, () => { + console.log(`users service running at http://localhost:${port}`); +}); diff --git a/server/services/users/src/migrations/20201120154305-create-user.js b/server/services/users/src/migrations/20201120154305-create-user.js new file mode 100644 index 0000000..c14310f --- /dev/null +++ b/server/services/users/src/migrations/20201120154305-create-user.js @@ -0,0 +1,36 @@ +'use strict'; +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.createTable('Users', { + id: { + allowNull: false, + autoIncrement: true, + primaryKey: true, + type: Sequelize.INTEGER + }, + username: { + type: Sequelize.STRING + }, + email: { + type: Sequelize.STRING + }, + password: { + type: Sequelize.STRING + }, + status: { + type: Sequelize.STRING + }, + createdAt: { + allowNull: false, + type: Sequelize.DATE + }, + updatedAt: { + allowNull: false, + type: Sequelize.DATE + } + }); + }, + down: async (queryInterface, Sequelize) => { + await queryInterface.dropTable('Users'); + } +}; \ No newline at end of file diff --git a/server/services/users/src/migrations/20201120154501-create-post.js b/server/services/users/src/migrations/20201120154501-create-post.js new file mode 100644 index 0000000..418006a --- /dev/null +++ b/server/services/users/src/migrations/20201120154501-create-post.js @@ -0,0 +1,30 @@ +'use strict'; +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.createTable('Posts', { + id: { + allowNull: false, + autoIncrement: true, + primaryKey: true, + type: Sequelize.INTEGER + }, + title: { + type: Sequelize.STRING + }, + content: { + type: Sequelize.STRING + }, + createdAt: { + allowNull: false, + type: Sequelize.DATE + }, + updatedAt: { + allowNull: false, + type: Sequelize.DATE + } + }); + }, + down: async (queryInterface, Sequelize) => { + await queryInterface.dropTable('Posts'); + } +}; \ No newline at end of file diff --git a/server/services/users/src/migrations/20201120155020-create-like.js b/server/services/users/src/migrations/20201120155020-create-like.js new file mode 100644 index 0000000..2f7cf74 --- /dev/null +++ b/server/services/users/src/migrations/20201120155020-create-like.js @@ -0,0 +1,27 @@ +'use strict'; +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.createTable('Likes', { + id: { + allowNull: false, + autoIncrement: true, + primaryKey: true, + type: Sequelize.INTEGER + }, + count: { + type: Sequelize.INTEGER + }, + createdAt: { + allowNull: false, + type: Sequelize.DATE + }, + updatedAt: { + allowNull: false, + type: Sequelize.DATE + } + }); + }, + down: async (queryInterface, Sequelize) => { + await queryInterface.dropTable('Likes'); + } +}; \ No newline at end of file diff --git a/server/services/users/src/migrations/20201120155127-create-comment.js b/server/services/users/src/migrations/20201120155127-create-comment.js new file mode 100644 index 0000000..b852648 --- /dev/null +++ b/server/services/users/src/migrations/20201120155127-create-comment.js @@ -0,0 +1,27 @@ +'use strict'; +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.createTable('Comments', { + id: { + allowNull: false, + autoIncrement: true, + primaryKey: true, + type: Sequelize.INTEGER + }, + content: { + type: Sequelize.STRING + }, + createdAt: { + allowNull: false, + type: Sequelize.DATE + }, + updatedAt: { + allowNull: false, + type: Sequelize.DATE + } + }); + }, + down: async (queryInterface, Sequelize) => { + await queryInterface.dropTable('Comments'); + } +}; \ No newline at end of file diff --git a/server/services/users/src/migrations/20201120155140-create-sub-comment.js b/server/services/users/src/migrations/20201120155140-create-sub-comment.js new file mode 100644 index 0000000..fb32ee4 --- /dev/null +++ b/server/services/users/src/migrations/20201120155140-create-sub-comment.js @@ -0,0 +1,27 @@ +'use strict'; +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.createTable('SubComments', { + id: { + allowNull: false, + autoIncrement: true, + primaryKey: true, + type: Sequelize.INTEGER + }, + content: { + type: Sequelize.STRING + }, + createdAt: { + allowNull: false, + type: Sequelize.DATE + }, + updatedAt: { + allowNull: false, + type: Sequelize.DATE + } + }); + }, + down: async (queryInterface, Sequelize) => { + await queryInterface.dropTable('SubComments'); + } +}; \ No newline at end of file diff --git a/server/services/users/src/models/comment.js b/server/services/users/src/models/comment.js new file mode 100644 index 0000000..17ac4ab --- /dev/null +++ b/server/services/users/src/models/comment.js @@ -0,0 +1,23 @@ +'use strict'; +const { + Model +} = require('sequelize'); +module.exports = (sequelize, DataTypes) => { + class Comment extends Model { + /** + * Helper method for defining associations. + * This method is not a part of Sequelize lifecycle. + * The `models/index` file will call this method automatically. + */ + static associate(models) { + // define association here + } + }; + Comment.init({ + content: DataTypes.STRING + }, { + sequelize, + modelName: 'Comment', + }); + return Comment; +}; \ No newline at end of file diff --git a/server/services/users/src/models/index.js b/server/services/users/src/models/index.js new file mode 100644 index 0000000..33f09e7 --- /dev/null +++ b/server/services/users/src/models/index.js @@ -0,0 +1,37 @@ +'use strict'; + +const fs = require('fs'); +const path = require('path'); +const Sequelize = require('sequelize'); +const basename = path.basename(__filename); +const env = process.env.NODE_ENV || 'development'; +const config = require(__dirname + '/../config/config.json')[env]; +const db = {}; + +let sequelize; +if (config.use_env_variable) { + sequelize = new Sequelize(process.env[config.use_env_variable], config); +} else { + sequelize = new Sequelize(config.database, config.username, config.password, config); +} + +fs + .readdirSync(__dirname) + .filter(file => { + return (file.indexOf('.') !== 0) && (file !== basename) && (file.slice(-3) === '.js'); + }) + .forEach(file => { + const model = require(path.join(__dirname, file))(sequelize, Sequelize.DataTypes); + db[model.name] = model; + }); + +Object.keys(db).forEach(modelName => { + if (db[modelName].associate) { + db[modelName].associate(db); + } +}); + +db.sequelize = sequelize; +db.Sequelize = Sequelize; + +module.exports = db; diff --git a/server/services/users/src/models/like.js b/server/services/users/src/models/like.js new file mode 100644 index 0000000..54b9b1e --- /dev/null +++ b/server/services/users/src/models/like.js @@ -0,0 +1,23 @@ +'use strict'; +const { + Model +} = require('sequelize'); +module.exports = (sequelize, DataTypes) => { + class Like extends Model { + /** + * Helper method for defining associations. + * This method is not a part of Sequelize lifecycle. + * The `models/index` file will call this method automatically. + */ + static associate(models) { + // define association here + } + }; + Like.init({ + count: DataTypes.INTEGER + }, { + sequelize, + modelName: 'Like', + }); + return Like; +}; \ No newline at end of file diff --git a/server/services/users/src/models/post.js b/server/services/users/src/models/post.js new file mode 100644 index 0000000..ef17d3f --- /dev/null +++ b/server/services/users/src/models/post.js @@ -0,0 +1,24 @@ +'use strict'; +const { + Model +} = require('sequelize'); +module.exports = (sequelize, DataTypes) => { + class Post extends Model { + /** + * Helper method for defining associations. + * This method is not a part of Sequelize lifecycle. + * The `models/index` file will call this method automatically. + */ + static associate(models) { + // define association here + } + }; + Post.init({ + title: DataTypes.STRING, + content: DataTypes.STRING + }, { + sequelize, + modelName: 'Post', + }); + return Post; +}; \ No newline at end of file diff --git a/server/services/users/src/models/subcomment.js b/server/services/users/src/models/subcomment.js new file mode 100644 index 0000000..5b5bb9f --- /dev/null +++ b/server/services/users/src/models/subcomment.js @@ -0,0 +1,23 @@ +'use strict'; +const { + Model +} = require('sequelize'); +module.exports = (sequelize, DataTypes) => { + class SubComment extends Model { + /** + * Helper method for defining associations. + * This method is not a part of Sequelize lifecycle. + * The `models/index` file will call this method automatically. + */ + static associate(models) { + // define association here + } + }; + SubComment.init({ + content: DataTypes.STRING + }, { + sequelize, + modelName: 'SubComment', + }); + return SubComment; +}; \ No newline at end of file diff --git a/server/services/users/src/models/user.js b/server/services/users/src/models/user.js new file mode 100644 index 0000000..8132ef7 --- /dev/null +++ b/server/services/users/src/models/user.js @@ -0,0 +1,26 @@ +'use strict'; +const { + Model +} = require('sequelize'); +module.exports = (sequelize, DataTypes) => { + class User extends Model { + /** + * Helper method for defining associations. + * This method is not a part of Sequelize lifecycle. + * The `models/index` file will call this method automatically. + */ + static associate(models) { + // define association here + } + }; + User.init({ + username: DataTypes.STRING, + email: DataTypes.STRING, + password: DataTypes.STRING, + status: DataTypes.STRING + }, { + sequelize, + modelName: 'User', + }); + return User; +}; \ No newline at end of file diff --git a/server/services/users/src/routes/index.js b/server/services/users/src/routes/index.js new file mode 100644 index 0000000..e15740b --- /dev/null +++ b/server/services/users/src/routes/index.js @@ -0,0 +1,7 @@ +const registerRoute = require('./register'); + +function loadRoutes(router) { + registerRoute(router); +}; + +module.exports = loadRoutes; diff --git a/server/services/users/src/routes/register/index.js b/server/services/users/src/routes/register/index.js new file mode 100644 index 0000000..8698ead --- /dev/null +++ b/server/services/users/src/routes/register/index.js @@ -0,0 +1,8 @@ +const UserController = require('../../controllers/UserController'); + +function registerRoute(router) { + router.get('/register', UserController.register); + // router.post('/register', UserController.register); +}; + +module.exports = registerRoute; From d89b02eaefcc61fef45c2dbc46ae6d044fd00b94 Mon Sep 17 00:00:00 2001 From: Alfian Tirta Date: Sat, 21 Nov 2020 15:59:39 +0700 Subject: [PATCH 02/37] initialized users rest api document --- server/services/users/api_doc.md | 504 +++++++++++++++++++++++++++++++ 1 file changed, 504 insertions(+) create mode 100644 server/services/users/api_doc.md diff --git a/server/services/users/api_doc.md b/server/services/users/api_doc.md new file mode 100644 index 0000000..2a593b6 --- /dev/null +++ b/server/services/users/api_doc.md @@ -0,0 +1,504 @@ +# Blog App - Users Service +Blog App is an application to manage your blog. It has two services : Users & Logs. + +Its Users service has : + +* RESTful endpoints for user registration, verification, and login. +* RESTful endpoints for CRUD operations of blog posts, likes, comments, and sub comments. +* JSON formatted response. + +  + +## Endpoints +``` + - POST /users/register + - PUT /users/verify?username=&email=&password= + - POST /users/login + + - POST /posts + - GET /posts + - GET /posts/:id + - PUT /posts/:id + - DELETE /posts/:id + + - POST /comments + - DELETE /comments/:id + + - POST /sub-comments + - DELETE /sub-comments/:id + + - POST /likes + - DELETE /likes/:id +``` + +## RESTful endpoints +### POST /users/register + +> Register user + +_Request Header_ +``` +not needed +``` + +_Request Body_ +``` +{ + "username": "", + "email": "", + "password": "", +} +``` + +_Response (201 - Created)_ +``` +{ + "id": "", + "username": "", + "email": "", + "password": "", + "status": "registered", +} +``` + +_Response (400 - Bad Request)_ +``` +[ + "", + "", + ..., + "" +] +``` + +_Response (500 - Internal Server Error)_ +``` +[ + "" +] +``` +--- +### PUT /users/verify?username=&email=&password= + +> Verify user + +_Request Header_ +``` +not needed +``` + +_Request Body_ +``` +not needed +``` + +_Response (200 - OK)_ +``` +{ + "id": "", + "username": "", + "email": "", + "password": "", + "status": "active", +} +``` + +_Response (400 - Bad Request)_ +``` +[ + "The verification link is invalid." +] +``` + +_Response (500 - Internal Server Error)_ +``` +[ + "" +] +``` +--- +### POST /users/login + +> Login user + +_Request Header_ +``` +not needed +``` + +_Request Body_ +``` +{ + "email": "", + "password": "" +} +``` + +_Response (200 - OK)_ +``` +{ + "access_token": "", + "user_id": "" +} +``` + +_Response (400 - Bad Request)_ +``` +[ + "The Email or Password is invalid." +] +``` + +_Response (500 - Internal Server Error)_ +``` +[ + "" +] +``` +--- +### POST /googleLogin + +> Login user with Google account + +_Request Header_ +``` +not needed +``` + +_Request Body_ +``` +not needed +``` + +_Response (200 - OK)_ +``` +{ + "access_token": "", + "UserId": "" +} +``` + +_Response (400 - Bad Request)_ +``` +[ + "The Email or Password is invalid." +] +``` + +_Response (500 - Internal Server Error)_ +``` +[ + "" +] +``` +--- +### POST /todos + +> Create new todo + +_Request Header_ +``` +{ + "access_token": "" +} +``` + +_Request Body_ +``` +{ + "title": "Learn REST API", + "description": "Learn how to create RESTful API with Express and Sequelize", + "status": "ongoing", + "due_date": "2020-01-29", + "UserId": "1" +} +``` + +_Response (201 - Created)_ +``` +{ + "id": "1", + "title": "Learn REST API", + "description": "Learn how to create RESTful API with Express and Sequelize", + "status": "ongoing", + "due_date": "2020-01-29T00:00:00.000Z", + "UserId": "1", + "createdAt": "2020-01-27T07:15:12.149Z", + "updatedAt": "2020-01-27T07:15:12.149Z", +} +``` + +_Response (401 - Unauthorized)_ +``` +[ + "The user is not authenticated" +] +``` + +_Response (403 - Forbidden)_ +``` +[ + "The user is not authorized." +] +``` + +_Response (500 - Internal Server Error)_ +``` +[ + "" +] +``` +--- +### GET /todos + +> Get all todos + +_Request Header_ +``` +{ + "access_token": "" +} +``` + +_Request Body_ +``` +not needed +``` + +_Response (200 - OK)_ +``` +[ + { + "id": "1", + "title": "Learn REST API", + "description": "Learn how to create RESTful API with Express and Sequelize", + "status": "ongoing", + "due_date": "2020-01-29T00:00:00.000Z", + "UserId": "1", + "createdAt": "2020-01-27T07:15:12.149Z", + "updatedAt": "2020-01-27T07:15:12.149Z", + }, + { + "id": "2", + "title": "Learn API Documentation", + "description": "Learn how to create API Documentation with REST standard", + "status": "done", + "due_date": "2020-01-29T00:00:00.000Z", + "UserId": "1", + "createdAt": "2020-01-28T07:15:12.149Z", + "updatedAt": "2020-01-28T07:15:12.149Z", + } +] +``` + +_Response (401 - Unauthorized)_ +``` +[ + "The user is not authenticated" +] +``` + +_Response (403 - Forbidden)_ +``` +[ + "The user is not authorized." +] +``` + +_Response (500 - Internal Server Error)_ +``` +[ + "" +] +``` +--- +### GET /todos/:id + +> Get todos by UserId + +_Request Header_ +``` +{ + "access_token": "" +} +``` + +_Request Body_ +``` +not needed +``` + +_Response (200 - OK)_ +``` +[ + { + "id": "1", + "title": "Learn REST API", + "description": "Learn how to create RESTful API with Express and Sequelize", + "status": "ongoing", + "due_date": "2020-01-29T00:00:00.000Z", + "UserId": "1", + "createdAt": "2020-01-27T07:15:12.149Z", + "updatedAt": "2020-01-27T07:15:12.149Z", + }, + { + "id": "2", + "title": "Learn API Documentation", + "description": "Learn how to create API Documentation with REST standard", + "status": "done", + "due_date": "2020-01-29T00:00:00.000Z", + "UserId": "1", + "createdAt": "2020-01-28T07:15:12.149Z", + "updatedAt": "2020-01-28T07:15:12.149Z", + } +] +``` + +_Response (400 - Bad Request)_ +``` +[ + "The user with id was not found." +] +``` + +_Response (401 - Unauthorized)_ +``` +[ + "The user is not authenticated" +] +``` + +_Response (403 - Forbidden)_ +``` +[ + "The user is not authorized." +] +``` + +_Response (500 - Internal Server Error)_ +``` +[ + "" +] +``` +--- +### PUT /todos/:id + +> Update todo by todo_id + +_Request Header_ +``` +{ + "access_token": "" +} +``` + +_Request Body_ +``` +{ + "title": "Learn Node", + "description": "Learn how to create app with Express and Sequelize", + "status": "ongoing", + "due_date": "2020-01-30" +} +``` + +_Response (200 - OK)_ +``` +{ + "id": "1", + "title": "Learn Node", + "description": "Learn how to create app with Express and Sequelize", + "status": "ongoing", + "due_date": "2020-01-30", + "UserId": "1", + "createdAt": "2020-01-27T07:15:12.149Z", + "updatedAt": "2020-01-29T07:15:12.149Z", +} +``` + +_Response (400 - Bad Request)_ +``` +[ + "The todo with id was not found." +] +``` + +_Response (401 - Unauthorized)_ +``` +[ + "The user is not authenticated" +] +``` + +_Response (403 - Forbidden)_ +``` +[ + "The user is not authorized." +] +``` + +_Response (500 - Internal Server Error)_ +``` +[ + "" +] +``` +--- +### DELETE /todos/:id + +> Delete todo by todo_id + +_Request Header_ +``` +{ + "access_token": "" +} +``` + +_Request Body_ +``` +not needed +``` + +_Response (200 - OK)_ +``` +{ + "id": "2", + "title": "Learn API Documentation", + "description": "Learn how to create API Documentation with REST standard", + "status": "done", + "due_date": "2020-01-29", + "UserId": "1", + "createdAt": "2020-01-28T07:15:12.149Z", + "updatedAt": "2020-01-28T07:15:12.149Z", +} +``` + +_Response (400 - Bad Request)_ +``` +[ + "The todo with id was not found." +] +``` + +_Response (401 - Unauthorized)_ +``` +[ + "The user is not authenticated" +] +``` + +_Response (403 - Forbidden)_ +``` +[ + "The user is not authorized." +] +``` + +_Response (500 - Internal Server Error)_ +``` +[ + "" +] +``` From 0cd906a14316eafa12d70552af9e598a2ae7077e Mon Sep 17 00:00:00 2001 From: Alfian Tirta Date: Sat, 21 Nov 2020 16:40:31 +0700 Subject: [PATCH 03/37] update users service db schema --- .../20201121090740-add-userid-to-posts.js | 19 +++++++++++++++++++ .../20201121090826-add-postid-to-likes.js | 19 +++++++++++++++++++ .../20201121090831-add-userid-to-likes.js | 19 +++++++++++++++++++ .../20201121090854-add-postid-to-comments.js | 19 +++++++++++++++++++ .../20201121090901-add-userid-to-comments.js | 19 +++++++++++++++++++ ...1121090912-add-commentid-to-subcomments.js | 19 +++++++++++++++++++ ...0201121091027-add-userid-to-subcomments.js | 19 +++++++++++++++++++ .../20201121091101-delete-count-from-likes.js | 11 +++++++++++ server/services/users/src/models/comment.js | 7 ++++++- server/services/users/src/models/like.js | 5 ++++- server/services/users/src/models/post.js | 6 +++++- .../services/users/src/models/subcomment.js | 6 +++++- server/services/users/src/models/user.js | 4 ++++ server/services/users/src/routes/index.js | 2 +- .../users/src/routes/register/index.js | 8 -------- .../services/users/src/routes/users/index.js | 8 ++++++++ 16 files changed, 177 insertions(+), 13 deletions(-) create mode 100644 server/services/users/src/migrations/20201121090740-add-userid-to-posts.js create mode 100644 server/services/users/src/migrations/20201121090826-add-postid-to-likes.js create mode 100644 server/services/users/src/migrations/20201121090831-add-userid-to-likes.js create mode 100644 server/services/users/src/migrations/20201121090854-add-postid-to-comments.js create mode 100644 server/services/users/src/migrations/20201121090901-add-userid-to-comments.js create mode 100644 server/services/users/src/migrations/20201121090912-add-commentid-to-subcomments.js create mode 100644 server/services/users/src/migrations/20201121091027-add-userid-to-subcomments.js create mode 100644 server/services/users/src/migrations/20201121091101-delete-count-from-likes.js delete mode 100644 server/services/users/src/routes/register/index.js create mode 100644 server/services/users/src/routes/users/index.js diff --git a/server/services/users/src/migrations/20201121090740-add-userid-to-posts.js b/server/services/users/src/migrations/20201121090740-add-userid-to-posts.js new file mode 100644 index 0000000..5ab3006 --- /dev/null +++ b/server/services/users/src/migrations/20201121090740-add-userid-to-posts.js @@ -0,0 +1,19 @@ +'use strict'; + +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.addColumn('Posts', 'UserId', { + type: Sequelize.INTEGER, + references: { + model: 'Users', + key: 'id', + }, + onUpdate: 'CASCADE', + onDelete: 'CASCADE', + }); + }, + + down: async (queryInterface, Sequelize) => { + await queryInterface.removeColumn('Posts', 'UserId'); + } +}; diff --git a/server/services/users/src/migrations/20201121090826-add-postid-to-likes.js b/server/services/users/src/migrations/20201121090826-add-postid-to-likes.js new file mode 100644 index 0000000..31fc82a --- /dev/null +++ b/server/services/users/src/migrations/20201121090826-add-postid-to-likes.js @@ -0,0 +1,19 @@ +'use strict'; + +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.addColumn('Likes', 'PostId', { + type: Sequelize.INTEGER, + references: { + model: 'Posts', + key: 'id', + }, + onUpdate: 'CASCADE', + onDelete: 'CASCADE', + }); + }, + + down: async (queryInterface, Sequelize) => { + await queryInterface.removeColumn('Likes', 'PostId'); + } +}; diff --git a/server/services/users/src/migrations/20201121090831-add-userid-to-likes.js b/server/services/users/src/migrations/20201121090831-add-userid-to-likes.js new file mode 100644 index 0000000..2040e72 --- /dev/null +++ b/server/services/users/src/migrations/20201121090831-add-userid-to-likes.js @@ -0,0 +1,19 @@ +'use strict'; + +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.addColumn('Likes', 'UserId', { + type: Sequelize.INTEGER, + references: { + model: 'Users', + key: 'id', + }, + onUpdate: 'CASCADE', + onDelete: 'CASCADE', + }); + }, + + down: async (queryInterface, Sequelize) => { + await queryInterface.removeColumn('Likes', 'UserId'); + } +}; diff --git a/server/services/users/src/migrations/20201121090854-add-postid-to-comments.js b/server/services/users/src/migrations/20201121090854-add-postid-to-comments.js new file mode 100644 index 0000000..a7f4a35 --- /dev/null +++ b/server/services/users/src/migrations/20201121090854-add-postid-to-comments.js @@ -0,0 +1,19 @@ +'use strict'; + +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.addColumn('Comments', 'PostId', { + type: Sequelize.INTEGER, + references: { + model: 'Posts', + key: 'id', + }, + onUpdate: 'CASCADE', + onDelete: 'CASCADE', + }); + }, + + down: async (queryInterface, Sequelize) => { + await queryInterface.removeColumn('Comments', 'PostId'); + } +}; diff --git a/server/services/users/src/migrations/20201121090901-add-userid-to-comments.js b/server/services/users/src/migrations/20201121090901-add-userid-to-comments.js new file mode 100644 index 0000000..0ce7e21 --- /dev/null +++ b/server/services/users/src/migrations/20201121090901-add-userid-to-comments.js @@ -0,0 +1,19 @@ +'use strict'; + +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.addColumn('Comments', 'UserId', { + type: Sequelize.INTEGER, + references: { + model: 'Users', + key: 'id', + }, + onUpdate: 'CASCADE', + onDelete: 'CASCADE', + }); + }, + + down: async (queryInterface, Sequelize) => { + await queryInterface.removeColumn('Comments', 'UserId'); + } +}; diff --git a/server/services/users/src/migrations/20201121090912-add-commentid-to-subcomments.js b/server/services/users/src/migrations/20201121090912-add-commentid-to-subcomments.js new file mode 100644 index 0000000..36451d9 --- /dev/null +++ b/server/services/users/src/migrations/20201121090912-add-commentid-to-subcomments.js @@ -0,0 +1,19 @@ +'use strict'; + +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.addColumn('SubComments', 'CommentId', { + type: Sequelize.INTEGER, + references: { + model: 'Comments', + key: 'id', + }, + onUpdate: 'CASCADE', + onDelete: 'CASCADE', + }); + }, + + down: async (queryInterface, Sequelize) => { + await queryInterface.removeColumn('Comments', 'CommentId'); + } +}; diff --git a/server/services/users/src/migrations/20201121091027-add-userid-to-subcomments.js b/server/services/users/src/migrations/20201121091027-add-userid-to-subcomments.js new file mode 100644 index 0000000..33a69dd --- /dev/null +++ b/server/services/users/src/migrations/20201121091027-add-userid-to-subcomments.js @@ -0,0 +1,19 @@ +'use strict'; + +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.addColumn('SubComments', 'UserId', { + type: Sequelize.INTEGER, + references: { + model: 'Users', + key: 'id', + }, + onUpdate: 'CASCADE', + onDelete: 'CASCADE', + }); + }, + + down: async (queryInterface, Sequelize) => { + await queryInterface.removeColumn('SubComments', 'UserId'); + } +}; diff --git a/server/services/users/src/migrations/20201121091101-delete-count-from-likes.js b/server/services/users/src/migrations/20201121091101-delete-count-from-likes.js new file mode 100644 index 0000000..56f7507 --- /dev/null +++ b/server/services/users/src/migrations/20201121091101-delete-count-from-likes.js @@ -0,0 +1,11 @@ +'use strict'; + +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.removeColumn('Likes', 'count'); + }, + + down: async (queryInterface, Sequelize) => { + await queryInterface.addColumn('Likes', 'count', Sequelize.INTEGER); + } +}; diff --git a/server/services/users/src/models/comment.js b/server/services/users/src/models/comment.js index 17ac4ab..8229e7e 100644 --- a/server/services/users/src/models/comment.js +++ b/server/services/users/src/models/comment.js @@ -11,10 +11,15 @@ module.exports = (sequelize, DataTypes) => { */ static associate(models) { // define association here + Comment.hasMany(models.SubComment); + Comment.belongsTo(models.Post); + Comment.belongsTo(models.User); } }; Comment.init({ - content: DataTypes.STRING + content: DataTypes.STRING, + PostId: DataTypes.INTEGER, + UserId: DataTypes.INTEGER, }, { sequelize, modelName: 'Comment', diff --git a/server/services/users/src/models/like.js b/server/services/users/src/models/like.js index 54b9b1e..a122480 100644 --- a/server/services/users/src/models/like.js +++ b/server/services/users/src/models/like.js @@ -11,10 +11,13 @@ module.exports = (sequelize, DataTypes) => { */ static associate(models) { // define association here + Like.belongsTo(models.Post); + Like.belongsTo(models.User); } }; Like.init({ - count: DataTypes.INTEGER + PostId: DataTypes.INTEGER, + UserId: DataTypes.INTEGER, }, { sequelize, modelName: 'Like', diff --git a/server/services/users/src/models/post.js b/server/services/users/src/models/post.js index ef17d3f..5f7ab59 100644 --- a/server/services/users/src/models/post.js +++ b/server/services/users/src/models/post.js @@ -11,11 +11,15 @@ module.exports = (sequelize, DataTypes) => { */ static associate(models) { // define association here + Post.hasMany(models.Like); + Post.hasMany(models.Comment); + Post.belongsTo(models.User); } }; Post.init({ title: DataTypes.STRING, - content: DataTypes.STRING + content: DataTypes.STRING, + UserId: DataTypes.INTEGER, }, { sequelize, modelName: 'Post', diff --git a/server/services/users/src/models/subcomment.js b/server/services/users/src/models/subcomment.js index 5b5bb9f..d87f776 100644 --- a/server/services/users/src/models/subcomment.js +++ b/server/services/users/src/models/subcomment.js @@ -11,10 +11,14 @@ module.exports = (sequelize, DataTypes) => { */ static associate(models) { // define association here + SubComment.belongsTo(models.Comment); + SubComment.belongsTo(models.User); } }; SubComment.init({ - content: DataTypes.STRING + content: DataTypes.STRING, + CommentId: DataTypes.INTEGER, + UserId: DataTypes.INTEGER, }, { sequelize, modelName: 'SubComment', diff --git a/server/services/users/src/models/user.js b/server/services/users/src/models/user.js index 8132ef7..adfd3aa 100644 --- a/server/services/users/src/models/user.js +++ b/server/services/users/src/models/user.js @@ -11,6 +11,10 @@ module.exports = (sequelize, DataTypes) => { */ static associate(models) { // define association here + User.hasMany(models.Post); + User.hasMany(models.Like); + User.hasMany(models.Comment); + User.hasMany(models.SubComment); } }; User.init({ diff --git a/server/services/users/src/routes/index.js b/server/services/users/src/routes/index.js index e15740b..80d32c4 100644 --- a/server/services/users/src/routes/index.js +++ b/server/services/users/src/routes/index.js @@ -1,4 +1,4 @@ -const registerRoute = require('./register'); +const usersRoute = require('./users'); function loadRoutes(router) { registerRoute(router); diff --git a/server/services/users/src/routes/register/index.js b/server/services/users/src/routes/register/index.js deleted file mode 100644 index 8698ead..0000000 --- a/server/services/users/src/routes/register/index.js +++ /dev/null @@ -1,8 +0,0 @@ -const UserController = require('../../controllers/UserController'); - -function registerRoute(router) { - router.get('/register', UserController.register); - // router.post('/register', UserController.register); -}; - -module.exports = registerRoute; diff --git a/server/services/users/src/routes/users/index.js b/server/services/users/src/routes/users/index.js new file mode 100644 index 0000000..537d778 --- /dev/null +++ b/server/services/users/src/routes/users/index.js @@ -0,0 +1,8 @@ +const Controller = require('../../controllers/UserController'); + +function registerRoute(router) { + router.get('/users/register', Controller.register); + // router.post('/register', Controller.register); +}; + +module.exports = registerRoute; From aa51a44a0ec7e94657e859ec1001af134eb0c1aa Mon Sep 17 00:00:00 2001 From: Alfian Tirta Date: Mon, 23 Nov 2020 08:13:54 +0700 Subject: [PATCH 04/37] added users service api doc, users endpoint, bcrypt, jwt, mailgun, error handler --- server/services/users/api_doc.md | 10 +- server/services/users/package-lock.json | 603 ++++++++++++++++++ server/services/users/package.json | 3 + server/services/users/src/config/config.js | 2 +- .../users/src/controllers/UserController.js | 93 ++- server/services/users/src/helpers/bcrypt.js | 16 + .../users/src/helpers/errorHandler.js | 24 + server/services/users/src/helpers/jwt.js | 16 + server/services/users/src/index.js | 12 - server/services/users/src/models/index.js | 5 +- server/services/users/src/models/user.js | 48 +- server/services/users/src/routes/index.js | 2 +- .../services/users/src/routes/users/index.js | 9 +- 13 files changed, 805 insertions(+), 38 deletions(-) create mode 100644 server/services/users/src/helpers/bcrypt.js create mode 100644 server/services/users/src/helpers/errorHandler.js create mode 100644 server/services/users/src/helpers/jwt.js diff --git a/server/services/users/api_doc.md b/server/services/users/api_doc.md index 2a593b6..81da9f4 100644 --- a/server/services/users/api_doc.md +++ b/server/services/users/api_doc.md @@ -46,7 +46,7 @@ _Request Body_ { "username": "", "email": "", - "password": "", + "password": "" } ``` @@ -58,6 +58,8 @@ _Response (201 - Created)_ "email": "", "password": "", "status": "registered", + "createdAt": "", + "updatedAt": "" } ``` @@ -74,7 +76,7 @@ _Response (400 - Bad Request)_ _Response (500 - Internal Server Error)_ ``` [ - "" + "Internal Server Error" ] ``` --- @@ -106,14 +108,14 @@ _Response (200 - OK)_ _Response (400 - Bad Request)_ ``` [ - "The verification link is invalid." + "The user verification link is invalid." ] ``` _Response (500 - Internal Server Error)_ ``` [ - "" + "Internal Server Error" ] ``` --- diff --git a/server/services/users/package-lock.json b/server/services/users/package-lock.json index 041f10d..3d3df05 100644 --- a/server/services/users/package-lock.json +++ b/server/services/users/package-lock.json @@ -39,6 +39,14 @@ "negotiator": "0.6.2" } }, + "agent-base": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.3.0.tgz", + "integrity": "sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg==", + "requires": { + "es6-promisify": "^5.0.0" + } + }, "ansi-align": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.0.tgz", @@ -91,12 +99,38 @@ "picomatch": "^2.0.4" } }, + "ast-types": { + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.14.2.tgz", + "integrity": "sha512-O0yuUDnZeQDL+ncNGlJ78BiO4jnYI3bvMsD5prT0/nsgijG/LpNBIr63gTjVTNsiGkgQhiyCShTgxt8oXOrklA==", + "requires": { + "tslib": "^2.0.1" + } + }, + "async": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz", + "integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==", + "requires": { + "lodash": "^4.17.14" + } + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" + }, "balanced-match": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", "dev": true }, + "bcryptjs": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", + "integrity": "sha1-mrVie5PmBiH/fNrF2pczAn3x0Ms=" + }, "binary-extensions": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.1.0.tgz", @@ -138,6 +172,11 @@ "fill-range": "^7.0.1" } }, + "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-writer": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/buffer-writer/-/buffer-writer-2.0.0.tgz", @@ -290,6 +329,14 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "requires": { + "delayed-stream": "~1.0.0" + } + }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -344,12 +391,22 @@ "resolved": "https://registry.npmjs.org/copy-to/-/copy-to-2.0.1.tgz", "integrity": "sha1-JoD7uAaKSNCGVrYJgJK9r8kG9KU=" }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + }, "crypto-random-string": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==", "dev": true }, + "data-uri-to-buffer": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-1.2.0.tgz", + "integrity": "sha512-vKQ9DTQPN1FLYiiEEOQ6IBGFqvjCa5rSK3cWMy/Nespm5d/x3dGFT9UBZnkLxCwua/IXBi2TYnwTEpsOvhC4UQ==" + }, "debug": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", @@ -378,12 +435,32 @@ "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", "dev": true }, + "deep-is": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", + "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=" + }, "defer-to-connect": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-1.1.3.tgz", "integrity": "sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ==", "dev": true }, + "degenerator": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-1.0.4.tgz", + "integrity": "sha1-/PSQo37OJmRk2cxDGrmMWBnO0JU=", + "requires": { + "ast-types": "0.x.x", + "escodegen": "1.x.x", + "esprima": "3.x.x" + } + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" + }, "delegates": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", @@ -424,6 +501,14 @@ "integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=", "dev": true }, + "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", @@ -449,6 +534,19 @@ "once": "^1.4.0" } }, + "es6-promise": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", + "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==" + }, + "es6-promisify": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz", + "integrity": "sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM=", + "requires": { + "es6-promise": "^4.0.3" + } + }, "escape-goat": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/escape-goat/-/escape-goat-2.1.1.tgz", @@ -460,6 +558,55 @@ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" }, + "escodegen": { + "version": "1.14.3", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.14.3.tgz", + "integrity": "sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==", + "requires": { + "esprima": "^4.0.1", + "estraverse": "^4.2.0", + "esutils": "^2.0.2", + "optionator": "^0.8.1", + "source-map": "~0.6.1" + }, + "dependencies": { + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==" + } + } + }, + "esprima": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-3.1.3.tgz", + "integrity": "sha1-/cpRzuYTOJXjyI1TXOSdv/YqRjM=" + }, + "estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==" + }, + "esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==" + }, + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=" + }, + "file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==" + }, "fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", @@ -469,6 +616,16 @@ "to-regex-range": "^5.0.1" } }, + "form-data": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz", + "integrity": "sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + } + }, "fresh": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", @@ -481,6 +638,33 @@ "dev": true, "optional": true }, + "ftp": { + "version": "0.3.10", + "resolved": "https://registry.npmjs.org/ftp/-/ftp-0.3.10.tgz", + "integrity": "sha1-kZfYYa2BQvPmPVqDv+TFn3MwiF0=", + "requires": { + "readable-stream": "1.1.x", + "xregexp": "2.0.0" + }, + "dependencies": { + "readable-stream": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" + } + } + }, "get-stream": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", @@ -490,6 +674,56 @@ "pump": "^3.0.0" } }, + "get-uri": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-2.0.4.tgz", + "integrity": "sha512-v7LT/s8kVjs+Tx0ykk1I+H/rbpzkHvuIq87LmeXptcf5sNWm9uQiwjNAt94SJPA1zOlCntmnOlJvVWKmzsxG8Q==", + "requires": { + "data-uri-to-buffer": "1", + "debug": "2", + "extend": "~3.0.2", + "file-uri-to-path": "1", + "ftp": "~0.3.10", + "readable-stream": "2" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, "glob-parent": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz", @@ -593,6 +827,24 @@ } } }, + "http-proxy-agent": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-2.1.0.tgz", + "integrity": "sha512-qwHbBLV7WviBl0rQsOzH6o5lwyOIvwp/BdFnvVxXORldu5TmjFfjzBcWUWS5kWAZhmv+JtiDhSuQCp4sBfbIgg==", + "requires": { + "agent-base": "4", + "debug": "3.1.0" + } + }, + "https-proxy-agent": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-3.0.1.tgz", + "integrity": "sha512-+ML2Rbh6DAuee7d07tYGEKOEi2voWPUGan+ExdPbPW6Z3svq+JCqr0v8WmKPOkz1vOVykPCBSuobe7G8GJUtVg==", + "requires": { + "agent-base": "^4.3.0", + "debug": "^3.1.0" + } + }, "iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -640,6 +892,11 @@ "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==", "dev": true }, + "ip": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz", + "integrity": "sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo=" + }, "is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", @@ -718,6 +975,11 @@ "integrity": "sha512-/2UGPSgmtqwo1ktx8NDHjuPwZWmHhO+gj0f93EkhLB5RgW9RZevWYYlIkS6zePc6U2WpOdQYIwHe9YC4DWEBVg==", "dev": true }, + "is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=" + }, "is-typedarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", @@ -730,12 +992,65 @@ "integrity": "sha512-VjSeb/lHmkoyd8ryPVIKvOCn4D1koMqY+vqyjjUfc3xyKtP4dYOxM44sZrnqQSzSds3xyOrUTLTC9LVCVgLngw==", "dev": true }, + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" + }, "json-buffer": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.0.tgz", "integrity": "sha1-Wx85evx11ne96Lz8Dkfh+aPZqJg=", "dev": true }, + "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": { + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" + } + } + }, + "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" + } + }, "kcors": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/kcors/-/kcors-2.2.2.tgz", @@ -857,17 +1172,100 @@ "package-json": "^6.3.0" } }, + "levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", + "requires": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + } + }, "lodash": { "version": "4.17.20", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==" }, + "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.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha1-DdOXEhPHxW34gJd9UEyI+0cal6w=" + }, "lowercase-keys": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz", "integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==", "dev": true }, + "lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "requires": { + "yallist": "^3.0.2" + } + }, + "mailgun-js": { + "version": "0.22.0", + "resolved": "https://registry.npmjs.org/mailgun-js/-/mailgun-js-0.22.0.tgz", + "integrity": "sha512-a2alg5nuTZA9Psa1pSEIEsbxr1Zrmqx4VkgGCQ30xVh0kIH7Bu57AYILo+0v8QLSdXtCyLaS+KVmdCrQo0uWFA==", + "requires": { + "async": "^2.6.1", + "debug": "^4.1.0", + "form-data": "^2.3.3", + "inflection": "~1.12.0", + "is-stream": "^1.1.0", + "path-proxy": "~1.0.0", + "promisify-call": "^2.0.2", + "proxy-agent": "^3.0.3", + "tsscmp": "^1.0.6" + }, + "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==" + } + } + }, "make-dir": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", @@ -952,6 +1350,11 @@ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==" }, + "netmask": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/netmask/-/netmask-1.0.6.tgz", + "integrity": "sha1-ICl+idhvb2QA8lDZ9Pa0wZRfzTU=" + }, "nodemon": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.6.tgz", @@ -1036,12 +1439,67 @@ "resolved": "https://registry.npmjs.org/only/-/only-0.0.2.tgz", "integrity": "sha1-Kv3oTQPlC5qO3EROMGEKcCle37Q=" }, + "optionator": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", + "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", + "requires": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.6", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "word-wrap": "~1.2.3" + } + }, "p-cancelable": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-1.1.0.tgz", "integrity": "sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw==", "dev": true }, + "pac-proxy-agent": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-3.0.1.tgz", + "integrity": "sha512-44DUg21G/liUZ48dJpUSjZnFfZro/0K5JTyFYLBcmh9+T6Ooi4/i4efwUiEy0+4oQusCBqWdhv16XohIj1GqnQ==", + "requires": { + "agent-base": "^4.2.0", + "debug": "^4.1.1", + "get-uri": "^2.0.0", + "http-proxy-agent": "^2.1.0", + "https-proxy-agent": "^3.0.0", + "pac-resolver": "^3.0.0", + "raw-body": "^2.2.0", + "socks-proxy-agent": "^4.0.1" + }, + "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==" + } + } + }, + "pac-resolver": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-3.0.0.tgz", + "integrity": "sha512-tcc38bsjuE3XZ5+4vP96OfhOugrX+JcnpUbhfuc4LuXBLQhoTthOstZeoQJBDnQUDYzYmdImKsbz0xSl1/9qeA==", + "requires": { + "co": "^4.6.0", + "degenerator": "^1.0.4", + "ip": "^1.1.5", + "netmask": "^1.0.6", + "thunkify": "^2.1.2" + } + }, "package-json": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/package-json/-/package-json-6.5.0.tgz", @@ -1072,6 +1530,21 @@ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" }, + "path-proxy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/path-proxy/-/path-proxy-1.0.0.tgz", + "integrity": "sha1-GOijaFn8nS8aU7SN7hOFQ8Ag3l4=", + "requires": { + "inflection": "~1.3.0" + }, + "dependencies": { + "inflection": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/inflection/-/inflection-1.3.8.tgz", + "integrity": "sha1-y9Fg2p91sUw8xjV41POWeEvzAU4=" + } + } + }, "path-to-regexp": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.2.0.tgz", @@ -1160,12 +1633,65 @@ "xtend": "^4.0.0" } }, + "prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=" + }, "prepend-http": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz", "integrity": "sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=", "dev": true }, + "process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + }, + "promisify-call": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/promisify-call/-/promisify-call-2.0.4.tgz", + "integrity": "sha1-1IwtRWUszM1SgB3ey9UzptS9X7o=", + "requires": { + "with-callback": "^1.0.2" + } + }, + "proxy-agent": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-3.1.1.tgz", + "integrity": "sha512-WudaR0eTsDx33O3EJE16PjBRZWcX8GqCEeERw1W3hZJgH/F2a46g7jty6UGty6NeJ4CKQy8ds2CJPMiyeqaTvw==", + "requires": { + "agent-base": "^4.2.0", + "debug": "4", + "http-proxy-agent": "^2.1.0", + "https-proxy-agent": "^3.0.0", + "lru-cache": "^5.1.1", + "pac-proxy-agent": "^3.0.1", + "proxy-from-env": "^1.0.0", + "socks-proxy-agent": "^4.0.1" + }, + "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==" + } + } + }, + "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==" + }, "pstree.remy": { "version": "1.1.8", "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", @@ -1370,6 +1896,45 @@ "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==", "dev": true }, + "smart-buffer": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.1.0.tgz", + "integrity": "sha512-iVICrxOzCynf/SNaBQCw34eM9jROU/s5rzIhpOvzhzuYHfJR/DhZfDkXiZSgKXfgv26HT3Yni3AV/DGw0cGnnw==" + }, + "socks": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.3.3.tgz", + "integrity": "sha512-o5t52PCNtVdiOvzMry7wU4aOqYWL0PeCXRWBEiJow4/i/wr+wpsJQ9awEu1EonLIqsfGd5qSgDdxEOvCdmBEpA==", + "requires": { + "ip": "1.1.5", + "smart-buffer": "^4.1.0" + } + }, + "socks-proxy-agent": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-4.0.2.tgz", + "integrity": "sha512-NT6syHhI9LmuEMSK6Kd2V7gNv5KFZoLE7V5udWmn0de+3Mkj3UMA/AJPLyeNUVmElCurSHtUdM3ETpR3z770Wg==", + "requires": { + "agent-base": "~4.2.1", + "socks": "~2.3.2" + }, + "dependencies": { + "agent-base": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.2.1.tgz", + "integrity": "sha512-JVwXMr9nHYTUXsBFKUqhJwvlcYU/blreOEUkhNR2eXZIvwd+c+o5V4MgDPKWnMS/56awN3TRzIP+KoPn+roQtg==", + "requires": { + "es6-promisify": "^5.0.0" + } + } + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "optional": true + }, "split2": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/split2/-/split2-3.2.2.tgz", @@ -1468,6 +2033,11 @@ "integrity": "sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==", "dev": true }, + "thunkify": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/thunkify/-/thunkify-2.1.2.tgz", + "integrity": "sha1-+qDp0jDFGsyVyhOjYawFyn4EVT0=" + }, "to-readable-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/to-readable-stream/-/to-readable-stream-1.0.0.tgz", @@ -1502,11 +2072,24 @@ "nopt": "~1.0.10" } }, + "tslib": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.3.tgz", + "integrity": "sha512-uZtkfKblCEQtZKBF6EBXVZeQNl82yqtDQdv+eck8u7tdPxjLu2/lp5/uPW+um2tpuxINHWy3GhiccY7QgEaVHQ==" + }, "tsscmp": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.6.tgz", "integrity": "sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==" }, + "type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", + "requires": { + "prelude-ls": "~1.1.2" + } + }, "type-fest": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", @@ -1624,6 +2207,11 @@ "string-width": "^4.0.0" } }, + "with-callback": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/with-callback/-/with-callback-1.0.2.tgz", + "integrity": "sha1-oJYpuakgAo1yFAT7Q1vc/1yRvCE=" + }, "wkx": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/wkx/-/wkx-0.5.0.tgz", @@ -1632,6 +2220,11 @@ "@types/node": "*" } }, + "word-wrap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", + "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==" + }, "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -1656,11 +2249,21 @@ "integrity": "sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==", "dev": true }, + "xregexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/xregexp/-/xregexp-2.0.0.tgz", + "integrity": "sha1-UqY+VsoLhKfzpfPWGHLxJq16WUM=" + }, "xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==" }, + "yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" + }, "ylru": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ylru/-/ylru-1.2.1.tgz", diff --git a/server/services/users/package.json b/server/services/users/package.json index 80f5d4e..60a2ee2 100644 --- a/server/services/users/package.json +++ b/server/services/users/package.json @@ -4,11 +4,14 @@ "description": "", "main": "index.js", "dependencies": { + "bcryptjs": "^2.4.3", "dotenv": "^8.2.0", + "jsonwebtoken": "^8.5.1", "kcors": "^2.2.2", "koa": "^2.13.0", "koa-bodyparser": "^4.3.0", "koa-router": "^10.0.0", + "mailgun-js": "^0.22.0", "pg": "^8.5.1", "sequelize": "^6.3.5" }, diff --git a/server/services/users/src/config/config.js b/server/services/users/src/config/config.js index 598eb28..a139214 100644 --- a/server/services/users/src/config/config.js +++ b/server/services/users/src/config/config.js @@ -6,7 +6,7 @@ module.exports = { "password": process.env.DB_PASSWORD, "database": process.env.DB_NAME, "host": process.env.DB_HOST, - "dialect": process.env.DB_DIALECT, + "dialect": "postgres", }, "test": { "username": "root", diff --git a/server/services/users/src/controllers/UserController.js b/server/services/users/src/controllers/UserController.js index 07b1eb8..ca76caa 100644 --- a/server/services/users/src/controllers/UserController.js +++ b/server/services/users/src/controllers/UserController.js @@ -1,18 +1,89 @@ +const { User } = require('../models'); +const { compare_bcrypt_password } = require('../helpers/bcrypt'); +const { generate_jwt_token } = require('../helpers/jwt'); +const errorHandler = require('../helpers/errorHandler'); + +// Mailgun API : +const api_key = `${process.env.MAILGUN_API_KEY}`; +const domain = `${process.env.MAILGUN_DOMAIN}`; +const mailgun = require('mailgun-js')({ + apiKey: api_key, + domain, +}); + class UserController { static async register(ctx) { - // const { username, email, password } = ctx.request.body; - // try { - // const new_user = await User.create({ username, email, password }); - // return ctx.body = { - // id: new_user.id, - // email: new_user.email, - // }; - // } catch(err) { - // console.log(err); - // } + const { username, email, password } = ctx.request.body; + try { + const new_user = await User.create({ username, email, password }); + + // Send email with Mailgun API + const data = { + from: `Blog App `, + to: `${new_user.email}`, + subject: `Blog App - User Verification`, + text: ` +Here is the link to verify your account : +(user verification link)`, + }; + mailgun.messages().send(data, (error, body) => { + if (error) { + console.log(error); + } else { + console.log(body); + } + }); + + ctx.response.status = 201; + ctx.body = new_user; + } catch(err) { + const { status, errors } = errorHandler(err); + ctx.response.status = status; + ctx.body = errors; + } } - static async login(ctx) {} + static async verify(ctx) { + const { username, email, password } = ctx.request.query; + try { + const user = await User.findOne({ where: { username, email }}); + if (!user) { + ctx.throw('The user verification link is invalid.'); + } else { + const password_matched = compare_bcrypt_password(password, user.password); + if (!password_matched) { + ctx.throw('The user verification link is invalid.'); + } else { + const user_activated = await User.update({ + status: 'active', + }, { + where: { + username, + email, + password: user.password, + }, + returning: true, + }); + ctx.response.status = 200; + ctx.body = user_activated[1][0]; + } + } + } catch(err) { + const { status, errors } = errorHandler(err); + ctx.response.status = status; + ctx.body = errors; + } + } + + static async login(ctx) { + const { email, password } = ctx.request.body; + try { + const user = await User.findOne({ where: { email }}); + // + } catch(err) { + console.log(err); + } + } } module.exports = UserController; diff --git a/server/services/users/src/helpers/bcrypt.js b/server/services/users/src/helpers/bcrypt.js new file mode 100644 index 0000000..54a9324 --- /dev/null +++ b/server/services/users/src/helpers/bcrypt.js @@ -0,0 +1,16 @@ +const bcrypt = require('bcryptjs'); + +function generate_bcrypt_hash(password) { + const salt = bcrypt.genSaltSync(10); + const hash = bcrypt.hashSync(password, salt); + return hash; +} + +function compare_bcrypt_password(password, hash) { + return bcrypt.compareSync(password, hash); +} + +module.exports = { + generate_bcrypt_hash, + compare_bcrypt_password, +}; diff --git a/server/services/users/src/helpers/errorHandler.js b/server/services/users/src/helpers/errorHandler.js new file mode 100644 index 0000000..f0365a4 --- /dev/null +++ b/server/services/users/src/helpers/errorHandler.js @@ -0,0 +1,24 @@ +function errorHandler(err) { + const errors = []; + let status; + console.log(err.name); + console.log(err.message); + switch(err.name) { + case 'SequelizeValidationError' : + err.errors.forEach(error => { + errors.push(error.message); + }); + status = 400; + break; + case 'InternalServerError' : + errors.push(err.message); + status = 400; + break; + default : + errors.push('Internal Server Error'); + status = 500; + } + return { status, errors }; +} + +module.exports = errorHandler; diff --git a/server/services/users/src/helpers/jwt.js b/server/services/users/src/helpers/jwt.js new file mode 100644 index 0000000..1fa3ae5 --- /dev/null +++ b/server/services/users/src/helpers/jwt.js @@ -0,0 +1,16 @@ +const jwt = require('jsonwebtoken'); +const secret_key = process.env.JWT_SECRET_KEY; + +function generate_jwt_token(userObject) { + const { id, username, email, status } = user; + return jwt.sign({ id, username, email, status }, secret_key); +} + +function verify_jwt_token(token) { + return jwt.verify(token, secret_key); +} + +module.exports = { + generate_jwt_token, + verify_jwt_token, +}; diff --git a/server/services/users/src/index.js b/server/services/users/src/index.js index a6e6a58..ba167a9 100644 --- a/server/services/users/src/index.js +++ b/server/services/users/src/index.js @@ -18,18 +18,6 @@ app loadRoutes(router); -// router -// .get('/', ctx => { -// ctx.body = 'Hello World'; -// }) -// .get('/aeiou', ctx => { -// ctx.body = 'aeiou'; -// }); -// -// router.get('/test', ctx => { -// ctx.body = 'test'; -// }); - app.listen(port, () => { console.log(`users service running at http://localhost:${port}`); }); diff --git a/server/services/users/src/models/index.js b/server/services/users/src/models/index.js index 33f09e7..662f182 100644 --- a/server/services/users/src/models/index.js +++ b/server/services/users/src/models/index.js @@ -5,14 +5,15 @@ const path = require('path'); const Sequelize = require('sequelize'); const basename = path.basename(__filename); const env = process.env.NODE_ENV || 'development'; -const config = require(__dirname + '/../config/config.json')[env]; +const config = require('../config/config')[env]; const db = {}; let sequelize; if (config.use_env_variable) { sequelize = new Sequelize(process.env[config.use_env_variable], config); } else { - sequelize = new Sequelize(config.database, config.username, config.password, config); + // sequelize = new Sequelize(config.database, config.username, config.password, config); + sequelize = new Sequelize('postgres://brfwayyd:3Ze-is7E93PJvsJm_uTUUDeyc9p6U6Mq@rosie.db.elephantsql.com:5432/brfwayyd'); } fs diff --git a/server/services/users/src/models/user.js b/server/services/users/src/models/user.js index adfd3aa..4e69566 100644 --- a/server/services/users/src/models/user.js +++ b/server/services/users/src/models/user.js @@ -2,6 +2,7 @@ const { Model } = require('sequelize'); +const { generate_bcrypt_hash } = require('../helpers/bcrypt'); module.exports = (sequelize, DataTypes) => { class User extends Model { /** @@ -18,13 +19,54 @@ module.exports = (sequelize, DataTypes) => { } }; User.init({ - username: DataTypes.STRING, - email: DataTypes.STRING, - password: DataTypes.STRING, + username: { + type: DataTypes.STRING, + allowNull: false, + unique: true, + validate: { + notEmpty: { + args: true, + msg: 'Username must not be empty.', + }, + }, + }, + email: { + type: DataTypes.STRING, + allowNull: false, + unique: true, + validate: { + isEmail: { + args: true, + msg: 'Please use the correct email format.', + }, + notEmpty: { + args: true, + msg: 'Email must not be empty.', + }, + }, + }, + password: { + type: DataTypes.STRING, + allowNull: false, + validate: { + len: { + args: [5], + msg: 'Password minimum length is 5 characters.' + }, + notEmpty: { + args: true, + msg: 'Password must not be empty.' + }, + }, + }, status: DataTypes.STRING }, { sequelize, modelName: 'User', }); + User.beforeCreate((user, options) => { + user.password = generate_bcrypt_hash(user.password); + user.status = 'registered'; + }); return User; }; \ No newline at end of file diff --git a/server/services/users/src/routes/index.js b/server/services/users/src/routes/index.js index 80d32c4..2ce0524 100644 --- a/server/services/users/src/routes/index.js +++ b/server/services/users/src/routes/index.js @@ -1,7 +1,7 @@ const usersRoute = require('./users'); function loadRoutes(router) { - registerRoute(router); + usersRoute(router); }; module.exports = loadRoutes; diff --git a/server/services/users/src/routes/users/index.js b/server/services/users/src/routes/users/index.js index 537d778..42d4881 100644 --- a/server/services/users/src/routes/users/index.js +++ b/server/services/users/src/routes/users/index.js @@ -1,8 +1,9 @@ const Controller = require('../../controllers/UserController'); -function registerRoute(router) { - router.get('/users/register', Controller.register); - // router.post('/register', Controller.register); +function usersRoute(router) { + router.post('/users/register', Controller.register); + router.put('/users/verify', Controller.verify); + // router.post('/users/login', Controller.login); }; -module.exports = registerRoute; +module.exports = usersRoute; From 9f98e0313e8ee195efea15235ac379a4e4151b00 Mon Sep 17 00:00:00 2001 From: Alfian Tirta Date: Mon, 23 Nov 2020 08:40:40 +0700 Subject: [PATCH 05/37] covered ElephantSQL URL --- server/services/users/src/models/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/services/users/src/models/index.js b/server/services/users/src/models/index.js index 662f182..f65e10b 100644 --- a/server/services/users/src/models/index.js +++ b/server/services/users/src/models/index.js @@ -13,7 +13,7 @@ if (config.use_env_variable) { sequelize = new Sequelize(process.env[config.use_env_variable], config); } else { // sequelize = new Sequelize(config.database, config.username, config.password, config); - sequelize = new Sequelize('postgres://brfwayyd:3Ze-is7E93PJvsJm_uTUUDeyc9p6U6Mq@rosie.db.elephantsql.com:5432/brfwayyd'); + sequelize = new Sequelize(process.env.ELEPHANTSQL_URL); } fs From 3900801d1579c2612dcacbab58bd40bc27099333 Mon Sep 17 00:00:00 2001 From: Alfian Tirta Date: Tue, 24 Nov 2020 08:20:07 +0700 Subject: [PATCH 06/37] user registration & verification features done --- server/services/users/api_doc.md | 2 +- .../users/src/controllers/UserController.js | 80 ++++++++----------- .../users/src/helpers/errorHandler.js | 7 +- server/services/users/src/helpers/jwt.js | 2 +- server/services/users/src/helpers/mailgun.js | 18 +++++ server/services/users/src/models/user.js | 36 +++++++-- .../services/users/src/routes/users/index.js | 2 +- 7 files changed, 88 insertions(+), 59 deletions(-) create mode 100644 server/services/users/src/helpers/mailgun.js diff --git a/server/services/users/api_doc.md b/server/services/users/api_doc.md index 81da9f4..49866f3 100644 --- a/server/services/users/api_doc.md +++ b/server/services/users/api_doc.md @@ -12,7 +12,7 @@ Its Users service has : ## Endpoints ``` - POST /users/register - - PUT /users/verify?username=&email=&password= + - GET /users/verify - POST /users/login - POST /posts diff --git a/server/services/users/src/controllers/UserController.js b/server/services/users/src/controllers/UserController.js index ca76caa..291d0d8 100644 --- a/server/services/users/src/controllers/UserController.js +++ b/server/services/users/src/controllers/UserController.js @@ -1,41 +1,29 @@ const { User } = require('../models'); const { compare_bcrypt_password } = require('../helpers/bcrypt'); -const { generate_jwt_token } = require('../helpers/jwt'); +const { generate_jwt_token, verify_jwt_token } = require('../helpers/jwt'); const errorHandler = require('../helpers/errorHandler'); - -// Mailgun API : -const api_key = `${process.env.MAILGUN_API_KEY}`; -const domain = `${process.env.MAILGUN_DOMAIN}`; -const mailgun = require('mailgun-js')({ - apiKey: api_key, - domain, -}); +const sendEmail = require('../helpers/mailgun'); class UserController { static async register(ctx) { const { username, email, password } = ctx.request.body; try { const new_user = await User.create({ username, email, password }); + ctx.response.status = 201; + ctx.body = new_user; + + // Generate user verification token : + const verification_token = generate_jwt_token(new_user); - // Send email with Mailgun API - const data = { + // Send email with Mailgun API : + const url = `http://localhost:3000/users/verify?token=${verification_token}`; + const email_data = { from: `Blog App `, to: `${new_user.email}`, subject: `Blog App - User Verification`, - text: ` -Here is the link to verify your account : -(user verification link)`, + text: `Please click on this link to verify your account : ${url}`, }; - mailgun.messages().send(data, (error, body) => { - if (error) { - console.log(error); - } else { - console.log(body); - } - }); - - ctx.response.status = 201; - ctx.body = new_user; + sendEmail(email_data); } catch(err) { const { status, errors } = errorHandler(err); ctx.response.status = status; @@ -44,30 +32,32 @@ Here is the link to verify your account : } static async verify(ctx) { - const { username, email, password } = ctx.request.query; + const { token } = ctx.request.query; try { - const user = await User.findOne({ where: { username, email }}); - if (!user) { - ctx.throw('The user verification link is invalid.'); - } else { - const password_matched = compare_bcrypt_password(password, user.password); - if (!password_matched) { - ctx.throw('The user verification link is invalid.'); - } else { - const user_activated = await User.update({ - status: 'active', - }, { - where: { - username, - email, - password: user.password, - }, - returning: true, - }); - ctx.response.status = 200; - ctx.body = user_activated[1][0]; + const decoded_user_data = verify_jwt_token(token); + const user = await User.findOne({ + where: { + username: decoded_user_data.username, + email: decoded_user_data.email, + status: decoded_user_data.status, } + }); + if (!user) { + throw new Error('The verification link is broken.'); + } + if (user.status === 'active') { + throw new Error('The user has already been verified.'); } + const user_activated = await User.update({ + status: 'active', + }, { + where: { + id: user.id, + }, + returning: true, + }); + ctx.response.status = 200; + ctx.body = 'User Verification Success'; } catch(err) { const { status, errors } = errorHandler(err); ctx.response.status = status; diff --git a/server/services/users/src/helpers/errorHandler.js b/server/services/users/src/helpers/errorHandler.js index f0365a4..0b45c19 100644 --- a/server/services/users/src/helpers/errorHandler.js +++ b/server/services/users/src/helpers/errorHandler.js @@ -1,6 +1,6 @@ function errorHandler(err) { const errors = []; - let status; + let status = 400; console.log(err.name); console.log(err.message); switch(err.name) { @@ -8,11 +8,12 @@ function errorHandler(err) { err.errors.forEach(error => { errors.push(error.message); }); - status = 400; break; case 'InternalServerError' : errors.push(err.message); - status = 400; + break; + case 'Error' : + errors.push(err.message); break; default : errors.push('Internal Server Error'); diff --git a/server/services/users/src/helpers/jwt.js b/server/services/users/src/helpers/jwt.js index 1fa3ae5..3b8f76c 100644 --- a/server/services/users/src/helpers/jwt.js +++ b/server/services/users/src/helpers/jwt.js @@ -2,7 +2,7 @@ const jwt = require('jsonwebtoken'); const secret_key = process.env.JWT_SECRET_KEY; function generate_jwt_token(userObject) { - const { id, username, email, status } = user; + const { id, username, email, status } = userObject; return jwt.sign({ id, username, email, status }, secret_key); } diff --git a/server/services/users/src/helpers/mailgun.js b/server/services/users/src/helpers/mailgun.js new file mode 100644 index 0000000..1035401 --- /dev/null +++ b/server/services/users/src/helpers/mailgun.js @@ -0,0 +1,18 @@ +const api_key = process.env.MAILGUN_API_KEY; +const domain = process.env.MAILGUN_DOMAIN; +const mailgun = require('mailgun-js')({ + apiKey: api_key, + domain, +}); + +function sendEmail(data_object) { + mailgun.messages().send(data_object, (error, body) => { + if (error) { + console.log(error); + } else { + console.log(body); + } + }); +} + +module.exports = sendEmail; diff --git a/server/services/users/src/models/user.js b/server/services/users/src/models/user.js index 4e69566..b5f9b8c 100644 --- a/server/services/users/src/models/user.js +++ b/server/services/users/src/models/user.js @@ -28,6 +28,16 @@ module.exports = (sequelize, DataTypes) => { args: true, msg: 'Username must not be empty.', }, + async isUsernameUnique(value) { + const user = await User.findOne({ + where: { + username: value, + }, + }); + if (user) { + throw new Error('Username must be unique.'); + } + }, }, }, email: { @@ -35,13 +45,23 @@ module.exports = (sequelize, DataTypes) => { allowNull: false, unique: true, validate: { + notEmpty: { + args: true, + msg: 'Email must not be empty.', + }, isEmail: { args: true, msg: 'Please use the correct email format.', }, - notEmpty: { - args: true, - msg: 'Email must not be empty.', + async isEmailUnique(value) { + const email = await User.findOne({ + where: { + email: value, + }, + }); + if (email) { + throw new Error('Email must be unique.'); + } }, }, }, @@ -49,14 +69,14 @@ module.exports = (sequelize, DataTypes) => { type: DataTypes.STRING, allowNull: false, validate: { - len: { - args: [5], - msg: 'Password minimum length is 5 characters.' - }, notEmpty: { args: true, msg: 'Password must not be empty.' }, + len: { + args: [5], + msg: 'Password minimum length is 5 characters.' + }, }, }, status: DataTypes.STRING @@ -64,7 +84,7 @@ module.exports = (sequelize, DataTypes) => { sequelize, modelName: 'User', }); - User.beforeCreate((user, options) => { + User.beforeCreate(async (user, options) => { user.password = generate_bcrypt_hash(user.password); user.status = 'registered'; }); diff --git a/server/services/users/src/routes/users/index.js b/server/services/users/src/routes/users/index.js index 42d4881..d39f98a 100644 --- a/server/services/users/src/routes/users/index.js +++ b/server/services/users/src/routes/users/index.js @@ -2,7 +2,7 @@ const Controller = require('../../controllers/UserController'); function usersRoute(router) { router.post('/users/register', Controller.register); - router.put('/users/verify', Controller.verify); + router.get('/users/verify', Controller.verify); // router.post('/users/login', Controller.login); }; From 5020ecb1e49b18bc0d725d5c372860a3c2a67608 Mon Sep 17 00:00:00 2001 From: Alfian Tirta Date: Tue, 24 Nov 2020 08:27:29 +0700 Subject: [PATCH 07/37] fixed bugs on user verification feature --- .../users/src/controllers/UserController.js | 29 +++++++++---------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/server/services/users/src/controllers/UserController.js b/server/services/users/src/controllers/UserController.js index 291d0d8..56bfd0f 100644 --- a/server/services/users/src/controllers/UserController.js +++ b/server/services/users/src/controllers/UserController.js @@ -39,25 +39,24 @@ class UserController { where: { username: decoded_user_data.username, email: decoded_user_data.email, - status: decoded_user_data.status, } }); if (!user) { - throw new Error('The verification link is broken.'); + ctx.body = 'The verification link is broken.'; + } else if (user.status === 'active') { + ctx.body = 'The user has already been verified.'; + } else { + const user_activated = await User.update({ + status: 'active', + }, { + where: { + id: user.id, + }, + returning: true, + }); + ctx.response.status = 200; + ctx.body = 'User Verification Success'; } - if (user.status === 'active') { - throw new Error('The user has already been verified.'); - } - const user_activated = await User.update({ - status: 'active', - }, { - where: { - id: user.id, - }, - returning: true, - }); - ctx.response.status = 200; - ctx.body = 'User Verification Success'; } catch(err) { const { status, errors } = errorHandler(err); ctx.response.status = status; From 12a5851c8d498e25427ef92cbc8fa8517250f988 Mon Sep 17 00:00:00 2001 From: Alfian Tirta Date: Tue, 24 Nov 2020 09:04:13 +0700 Subject: [PATCH 08/37] user login feature done --- server/services/users/api_doc.md | 22 ++++++----- .../users/src/controllers/UserController.js | 37 +++++++++++++------ .../users/src/helpers/errorHandler.js | 2 - .../services/users/src/routes/users/index.js | 2 +- 4 files changed, 40 insertions(+), 23 deletions(-) diff --git a/server/services/users/api_doc.md b/server/services/users/api_doc.md index 49866f3..ce19c0e 100644 --- a/server/services/users/api_doc.md +++ b/server/services/users/api_doc.md @@ -80,7 +80,7 @@ _Response (500 - Internal Server Error)_ ] ``` --- -### PUT /users/verify?username=&email=&password= +### GET /users/verify > Verify user @@ -97,18 +97,18 @@ not needed _Response (200 - OK)_ ``` { - "id": "", - "username": "", - "email": "", - "password": "", - "status": "active", + "message": "User Verification Success" } ``` _Response (400 - Bad Request)_ ``` [ - "The user verification link is invalid." + "The verification link is invalid." +] +or +[ + "The account has already been verified." ] ``` @@ -147,14 +147,18 @@ _Response (200 - OK)_ _Response (400 - Bad Request)_ ``` [ - "The Email or Password is invalid." + "The email or password is invalid." +] +or +[ + "Please verify your account." ] ``` _Response (500 - Internal Server Error)_ ``` [ - "" + "Internal Server Error" ] ``` --- diff --git a/server/services/users/src/controllers/UserController.js b/server/services/users/src/controllers/UserController.js index 56bfd0f..5cfef09 100644 --- a/server/services/users/src/controllers/UserController.js +++ b/server/services/users/src/controllers/UserController.js @@ -41,21 +41,19 @@ class UserController { email: decoded_user_data.email, } }); + + // Validate the token : if (!user) { - ctx.body = 'The verification link is broken.'; + throw new Error('The verification link is invalid.'); } else if (user.status === 'active') { - ctx.body = 'The user has already been verified.'; + throw new Error('The account has already been verified.'); } else { - const user_activated = await User.update({ - status: 'active', - }, { - where: { - id: user.id, - }, + const user_activated = await User.update({ status: 'active' }, { + where: { id: user.id }, returning: true, }); ctx.response.status = 200; - ctx.body = 'User Verification Success'; + ctx.body = { message: 'User Verification Success' }; } } catch(err) { const { status, errors } = errorHandler(err); @@ -68,9 +66,26 @@ class UserController { const { email, password } = ctx.request.body; try { const user = await User.findOne({ where: { email }}); - // + + // Validate the given email and password : + if (!user) { + throw new Error('The email or password is invalid.'); + } else { + const password_matched = compare_bcrypt_password(password, user.password); + if (!password_matched) { + throw new Error('The email or password is invalid.'); + } else if (password_matched && user.status !== 'active') { + throw new Error('Please verify your account.'); + } else { + const access_token = generate_jwt_token(user); + ctx.response.status = 200; + ctx.body = { access_token, user_id: user.id }; + } + } } catch(err) { - console.log(err); + const { status, errors } = errorHandler(err); + ctx.response.status = status; + ctx.body = errors; } } } diff --git a/server/services/users/src/helpers/errorHandler.js b/server/services/users/src/helpers/errorHandler.js index 0b45c19..41b35df 100644 --- a/server/services/users/src/helpers/errorHandler.js +++ b/server/services/users/src/helpers/errorHandler.js @@ -1,8 +1,6 @@ function errorHandler(err) { const errors = []; let status = 400; - console.log(err.name); - console.log(err.message); switch(err.name) { case 'SequelizeValidationError' : err.errors.forEach(error => { diff --git a/server/services/users/src/routes/users/index.js b/server/services/users/src/routes/users/index.js index d39f98a..1daedbe 100644 --- a/server/services/users/src/routes/users/index.js +++ b/server/services/users/src/routes/users/index.js @@ -3,7 +3,7 @@ const Controller = require('../../controllers/UserController'); function usersRoute(router) { router.post('/users/register', Controller.register); router.get('/users/verify', Controller.verify); - // router.post('/users/login', Controller.login); + router.post('/users/login', Controller.login); }; module.exports = usersRoute; From 90d449c8ecd2a862f21dda7ffddfe1d35954acf2 Mon Sep 17 00:00:00 2001 From: Alfian Tirta Date: Tue, 24 Nov 2020 21:34:35 +0700 Subject: [PATCH 09/37] added posts endpoints, updated users service api doc, added authentication middleware --- server/services/users/api_doc.md | 292 ++++++++---------- .../users/src/controllers/PostController.js | 94 ++++++ .../users/src/helpers/errorHandler.js | 6 + .../users/src/middlewares/authentication.js | 26 ++ server/services/users/src/routes/index.js | 2 + .../services/users/src/routes/posts/index.js | 15 + .../services/users/src/routes/users/index.js | 7 +- 7 files changed, 272 insertions(+), 170 deletions(-) create mode 100644 server/services/users/src/controllers/PostController.js create mode 100644 server/services/users/src/middlewares/authentication.js create mode 100644 server/services/users/src/routes/posts/index.js diff --git a/server/services/users/api_doc.md b/server/services/users/api_doc.md index ce19c0e..43a5fc0 100644 --- a/server/services/users/api_doc.md +++ b/server/services/users/api_doc.md @@ -12,12 +12,13 @@ Its Users service has : ## Endpoints ``` - POST /users/register - - GET /users/verify + - GET /users/verify?token= - POST /users/login - POST /posts - GET /posts - GET /posts/:id + - GET /posts/user/:id - PUT /posts/:id - DELETE /posts/:id @@ -53,13 +54,13 @@ _Request Body_ _Response (201 - Created)_ ``` { - "id": "", + "id": , "username": "", "email": "", "password": "", "status": "registered", - "createdAt": "", - "updatedAt": "" + "createdAt": "", + "updatedAt": "" } ``` @@ -80,7 +81,7 @@ _Response (500 - Internal Server Error)_ ] ``` --- -### GET /users/verify +### GET /users/verify?token= > Verify user @@ -162,107 +163,116 @@ _Response (500 - Internal Server Error)_ ] ``` --- -### POST /googleLogin +### POST /posts -> Login user with Google account +> Create blog post _Request Header_ ``` -not needed +{ + "access_token": "" +} ``` _Request Body_ ``` -not needed +{ + "title": "", + "content": "" +} ``` -_Response (200 - OK)_ +_Response (201 - Created)_ ``` { - "access_token": "", - "UserId": "" + "id": 1, + "title": "", + "content": "", + "UserId": , + "createdAt": "", + "updatedAt": "" } ``` -_Response (400 - Bad Request)_ +_Response (401 - Unauthorized)_ ``` [ - "The Email or Password is invalid." + "The user is not authenticated." ] ``` _Response (500 - Internal Server Error)_ ``` [ - "" + "Internal Server Error" ] ``` --- -### POST /todos +### GET /posts -> Create new todo +> Get all blog posts _Request Header_ ``` { - "access_token": "" + "access_token": "" } ``` _Request Body_ ``` -{ - "title": "Learn REST API", - "description": "Learn how to create RESTful API with Express and Sequelize", - "status": "ongoing", - "due_date": "2020-01-29", - "UserId": "1" -} -``` - -_Response (201 - Created)_ -``` -{ - "id": "1", - "title": "Learn REST API", - "description": "Learn how to create RESTful API with Express and Sequelize", - "status": "ongoing", - "due_date": "2020-01-29T00:00:00.000Z", - "UserId": "1", - "createdAt": "2020-01-27T07:15:12.149Z", - "updatedAt": "2020-01-27T07:15:12.149Z", -} +not needed ``` -_Response (401 - Unauthorized)_ +_Response (200 - OK)_ ``` [ - "The user is not authenticated" + { + "id": , + "title": "", + "content": "", + "UserId": , + "createdAt": "", + "updatedAt": "", + "User": { + "id": , + "username": "", + "email": "", + "password": "", + "status": "", + "createdAt": "", + "updatedAt": "" + }, + "Likes": [array of post likes], + "Comments": [array of post comments] + }, + ... ] ``` -_Response (403 - Forbidden)_ +_Response (401 - Unauthorized)_ ``` [ - "The user is not authorized." + "The user is not authenticated." ] ``` + _Response (500 - Internal Server Error)_ ``` [ - "" + "Internal Server Error" ] ``` --- -### GET /todos +### GET /posts/:id -> Get all todos +> Get a blog post by its id _Request Header_ ``` { - "access_token": "" + "access_token": "" } ``` @@ -273,59 +283,50 @@ not needed _Response (200 - OK)_ ``` -[ - { - "id": "1", - "title": "Learn REST API", - "description": "Learn how to create RESTful API with Express and Sequelize", - "status": "ongoing", - "due_date": "2020-01-29T00:00:00.000Z", - "UserId": "1", - "createdAt": "2020-01-27T07:15:12.149Z", - "updatedAt": "2020-01-27T07:15:12.149Z", +{ + "id": , + "title": "", + "content": "", + "UserId": , + "createdAt": "", + "updatedAt": "", + "User": { + "id": , + "username": "", + "email": "", + "password": "", + "status": "", + "createdAt": "", + "updatedAt": "" }, - { - "id": "2", - "title": "Learn API Documentation", - "description": "Learn how to create API Documentation with REST standard", - "status": "done", - "due_date": "2020-01-29T00:00:00.000Z", - "UserId": "1", - "createdAt": "2020-01-28T07:15:12.149Z", - "updatedAt": "2020-01-28T07:15:12.149Z", - } -] + "Likes": [array of post likes], + "Comments": [array of post comments] +} ``` -_Response (401 - Unauthorized)_ -``` -[ - "The user is not authenticated" -] -``` -_Response (403 - Forbidden)_ +_Response (401 - Unauthorized)_ ``` [ - "The user is not authorized." + "The user is not authenticated." ] ``` _Response (500 - Internal Server Error)_ ``` [ - "" + "Internal Server Error" ] ``` --- -### GET /todos/:id +### GET /posts/user/:id -> Get todos by UserId +> Get blog posts by user id _Request Header_ ``` { - "access_token": "" + "access_token": "" } ``` @@ -338,127 +339,98 @@ _Response (200 - OK)_ ``` [ { - "id": "1", - "title": "Learn REST API", - "description": "Learn how to create RESTful API with Express and Sequelize", - "status": "ongoing", - "due_date": "2020-01-29T00:00:00.000Z", - "UserId": "1", - "createdAt": "2020-01-27T07:15:12.149Z", - "updatedAt": "2020-01-27T07:15:12.149Z", + "id": , + "title": "", + "content": "", + "UserId": , + "createdAt": "", + "updatedAt": "", + "User": { + "id": , + "username": "", + "email": "", + "password": "", + "status": "", + "createdAt": "", + "updatedAt": "" + }, + "Likes": [array of post likes], + "Comments": [array of post comments] }, - { - "id": "2", - "title": "Learn API Documentation", - "description": "Learn how to create API Documentation with REST standard", - "status": "done", - "due_date": "2020-01-29T00:00:00.000Z", - "UserId": "1", - "createdAt": "2020-01-28T07:15:12.149Z", - "updatedAt": "2020-01-28T07:15:12.149Z", - } + ... ] ``` -_Response (400 - Bad Request)_ -``` -[ - "The user with id was not found." -] -``` _Response (401 - Unauthorized)_ ``` [ - "The user is not authenticated" -] -``` - -_Response (403 - Forbidden)_ -``` -[ - "The user is not authorized." + "The user is not authenticated." ] ``` _Response (500 - Internal Server Error)_ ``` [ - "" + "Internal Server Error" ] ``` --- -### PUT /todos/:id +### PUT /posts/:id -> Update todo by todo_id +> Update a blog post by its id _Request Header_ ``` { - "access_token": "" + "access_token": "" } ``` _Request Body_ ``` { - "title": "Learn Node", - "description": "Learn how to create app with Express and Sequelize", - "status": "ongoing", - "due_date": "2020-01-30" + "title": "", + "content": "" } ``` _Response (200 - OK)_ ``` { - "id": "1", - "title": "Learn Node", - "description": "Learn how to create app with Express and Sequelize", - "status": "ongoing", - "due_date": "2020-01-30", - "UserId": "1", - "createdAt": "2020-01-27T07:15:12.149Z", - "updatedAt": "2020-01-29T07:15:12.149Z", + "id": , + "title": "", + "content": "", + "UserId": , + "createdAt": "", + "updatedAt": "" } ``` -_Response (400 - Bad Request)_ -``` -[ - "The todo with id was not found." -] -``` _Response (401 - Unauthorized)_ ``` [ - "The user is not authenticated" + "The user is not authenticated." ] ``` -_Response (403 - Forbidden)_ -``` -[ - "The user is not authorized." -] -``` _Response (500 - Internal Server Error)_ ``` [ - "" + "Internal Server Error" ] ``` --- -### DELETE /todos/:id +### DELETE /posts/:id -> Delete todo by todo_id +> Delete a blog post by its id _Request Header_ ``` { - "access_token": "" + "access_token": "" } ``` @@ -470,41 +442,27 @@ not needed _Response (200 - OK)_ ``` { - "id": "2", - "title": "Learn API Documentation", - "description": "Learn how to create API Documentation with REST standard", - "status": "done", - "due_date": "2020-01-29", - "UserId": "1", - "createdAt": "2020-01-28T07:15:12.149Z", - "updatedAt": "2020-01-28T07:15:12.149Z", + "id": , + "title": "", + "content": "", + "UserId": , + "createdAt": "", + "updatedAt": "" } ``` -_Response (400 - Bad Request)_ -``` -[ - "The todo with id was not found." -] -``` _Response (401 - Unauthorized)_ ``` [ - "The user is not authenticated" + "The user is not authenticated." ] ``` -_Response (403 - Forbidden)_ -``` -[ - "The user is not authorized." -] -``` _Response (500 - Internal Server Error)_ ``` [ - "" + "Internal Server Error" ] ``` diff --git a/server/services/users/src/controllers/PostController.js b/server/services/users/src/controllers/PostController.js new file mode 100644 index 0000000..2df15cb --- /dev/null +++ b/server/services/users/src/controllers/PostController.js @@ -0,0 +1,94 @@ +const { Post, User, Like, Comment } = require('../models'); +const errorHandler = require('../helpers/errorHandler'); + +class PostController { + static async create(ctx) { + const { title, content } = ctx.request.body; + const UserId = ctx.user.id; + try { + const new_post = await Post.create({ title, content, UserId }); + ctx.response.status = 201; + ctx.body = new_post; + } catch(err) { + const { status, errors } = errorHandler(err); + ctx.response.status = status; + ctx.body = errors; + } + } + + static async read(ctx) { + try { + const all_posts = await Post.findAll({ include: [User, Like, Comment] }); + ctx.response.status = 200; + ctx.body = all_posts; + } catch(err) { + const { status, errors } = errorHandler(err); + ctx.response.status = status; + ctx.body = errors; + } + } + + static async findByPostId(ctx) { + const id = +ctx.request.params.id; + try { + const posts = await Post.findByPk(id, { + include: [User, Like, Comment], + }); + ctx.response.status = 200; + ctx.body = posts; + } catch(err) { + const { status, errors } = errorHandler(err); + ctx.response.status = status; + ctx.body = errors; + } + } + + static async findByUserId(ctx) { + const id = +ctx.request.params.id; + try { + const posts = await Post.findAll({ + where: { UserId: id }, + include: [User, Like, Comment], + }); + ctx.response.status = 200; + ctx.body = posts; + } catch(err) { + const { status, errors } = errorHandler(err); + ctx.response.status = status; + ctx.body = errors; + } + } + + static async update(ctx) { + const id = +ctx.request.params.id; + const { title, content } = ctx.request.body; + try { + const updated_post = await Post.update({ title, content },{ + where: { id }, + returning: true, + }); + ctx.response.status = 200; + ctx.body = updated_post[1][0]; + } catch(err) { + const { status, errors } = errorHandler(err); + ctx.response.status = status; + ctx.body = errors; + } + } + + static async delete(ctx) { + const id = +ctx.request.params.id; + try { + const deleted_post = await Post.findByPk(id); + await Post.destroy({ where: { id } }); + ctx.response.status = 200; + ctx.body = deleted_post; + } catch(err) { + const { status, errors } = errorHandler(err); + ctx.response.status = status; + ctx.body = errors; + } + } +} + +module.exports = PostController; diff --git a/server/services/users/src/helpers/errorHandler.js b/server/services/users/src/helpers/errorHandler.js index 41b35df..65783ba 100644 --- a/server/services/users/src/helpers/errorHandler.js +++ b/server/services/users/src/helpers/errorHandler.js @@ -7,11 +7,17 @@ function errorHandler(err) { errors.push(error.message); }); break; + case 'JsonWebTokenError' : + errors.push(err.message); + break; case 'InternalServerError' : errors.push(err.message); break; case 'Error' : errors.push(err.message); + if (err.message === 'The user is not authenticated.') { + status = 401; + } break; default : errors.push('Internal Server Error'); diff --git a/server/services/users/src/middlewares/authentication.js b/server/services/users/src/middlewares/authentication.js new file mode 100644 index 0000000..46b4ad2 --- /dev/null +++ b/server/services/users/src/middlewares/authentication.js @@ -0,0 +1,26 @@ +const { User } = require('../models'); +const { verify_jwt_token } = require('../helpers/jwt'); +const errorHandler = require('../helpers/errorHandler'); + +async function authentication(ctx, next) { + const { access_token } = ctx.request.header; + try { + const decoded_user_data = verify_jwt_token(access_token); + const user = await User.findOne({ + where: { + email: decoded_user_data.email, + } + }); + if (!user) { + throw new Error('The user is not authenticated.'); + } + ctx.user = user; + await next(); + } catch(err) { + const { status, errors } = errorHandler(err); + ctx.response.status = status; + ctx.body = errors; + } +} + +module.exports = authentication; diff --git a/server/services/users/src/routes/index.js b/server/services/users/src/routes/index.js index 2ce0524..2fd55de 100644 --- a/server/services/users/src/routes/index.js +++ b/server/services/users/src/routes/index.js @@ -1,7 +1,9 @@ const usersRoute = require('./users'); +const postsRoute = require('./posts'); function loadRoutes(router) { usersRoute(router); + postsRoute(router); }; module.exports = loadRoutes; diff --git a/server/services/users/src/routes/posts/index.js b/server/services/users/src/routes/posts/index.js new file mode 100644 index 0000000..954b9d4 --- /dev/null +++ b/server/services/users/src/routes/posts/index.js @@ -0,0 +1,15 @@ +const Controller = require('../../controllers/PostController'); +const authentication = require('../../middlewares/authentication'); + +function postsRoute(router) { + router + .use(authentication) + .post('/posts', Controller.create) + .get('/posts', Controller.read) + .get('/posts/:id', Controller.findByPostId) + .get('/posts/user/:id', Controller.findByUserId) + .put('/posts/:id', Controller.update) + .delete('/posts/:id', Controller.delete); +} + +module.exports = postsRoute; diff --git a/server/services/users/src/routes/users/index.js b/server/services/users/src/routes/users/index.js index 1daedbe..f16cb44 100644 --- a/server/services/users/src/routes/users/index.js +++ b/server/services/users/src/routes/users/index.js @@ -1,9 +1,10 @@ const Controller = require('../../controllers/UserController'); function usersRoute(router) { - router.post('/users/register', Controller.register); - router.get('/users/verify', Controller.verify); - router.post('/users/login', Controller.login); + router + .post('/users/register', Controller.register) + .get('/users/verify', Controller.verify) + .post('/users/login', Controller.login); }; module.exports = usersRoute; From ae4919806094ce466e60fc58f86f6e5f84aeb2c6 Mon Sep 17 00:00:00 2001 From: Alfian Tirta Date: Wed, 25 Nov 2020 09:12:29 +0700 Subject: [PATCH 10/37] added posts validation --- server/services/users/api_doc.md | 58 ++++++++++++++++++- .../users/src/controllers/PostController.js | 6 ++ server/services/users/src/models/post.js | 30 +++++++++- server/services/users/src/models/user.js | 12 ++++ 4 files changed, 102 insertions(+), 4 deletions(-) diff --git a/server/services/users/api_doc.md b/server/services/users/api_doc.md index 43a5fc0..c80af1f 100644 --- a/server/services/users/api_doc.md +++ b/server/services/users/api_doc.md @@ -194,6 +194,16 @@ _Response (201 - Created)_ } ``` +_Response (400 - Bad Request)_ +``` +[ + "", + "", + ..., + "" +] +``` + _Response (401 - Unauthorized)_ ``` [ @@ -250,6 +260,16 @@ _Response (200 - OK)_ ] ``` +_Response (400 - Bad Request)_ +``` +[ + "", + "", + ..., + "" +] +``` + _Response (401 - Unauthorized)_ ``` [ @@ -304,6 +324,15 @@ _Response (200 - OK)_ } ``` +_Response (400 - Bad Request)_ +``` +[ + "", + "", + ..., + "" +] +``` _Response (401 - Unauthorized)_ ``` @@ -361,6 +390,15 @@ _Response (200 - OK)_ ] ``` +_Response (400 - Bad Request)_ +``` +[ + "", + "", + ..., + "" +] +``` _Response (401 - Unauthorized)_ ``` @@ -407,6 +445,15 @@ _Response (200 - OK)_ } ``` +_Response (400 - Bad Request)_ +``` +[ + "", + "", + ..., + "" +] +``` _Response (401 - Unauthorized)_ ``` @@ -415,7 +462,6 @@ _Response (401 - Unauthorized)_ ] ``` - _Response (500 - Internal Server Error)_ ``` [ @@ -451,6 +497,15 @@ _Response (200 - OK)_ } ``` +_Response (400 - Bad Request)_ +``` +[ + "", + "", + ..., + "" +] +``` _Response (401 - Unauthorized)_ ``` @@ -459,7 +514,6 @@ _Response (401 - Unauthorized)_ ] ``` - _Response (500 - Internal Server Error)_ ``` [ diff --git a/server/services/users/src/controllers/PostController.js b/server/services/users/src/controllers/PostController.js index 2df15cb..e33ada4 100644 --- a/server/services/users/src/controllers/PostController.js +++ b/server/services/users/src/controllers/PostController.js @@ -61,6 +61,12 @@ class PostController { static async update(ctx) { const id = +ctx.request.params.id; + if (ctx.request.body.title === undefined) { + ctx.request.body.title = null; + } + if (ctx.request.body.content === undefined) { + ctx.request.body.content = null; + } const { title, content } = ctx.request.body; try { const updated_post = await Post.update({ title, content },{ diff --git a/server/services/users/src/models/post.js b/server/services/users/src/models/post.js index 5f7ab59..b5df364 100644 --- a/server/services/users/src/models/post.js +++ b/server/services/users/src/models/post.js @@ -17,8 +17,34 @@ module.exports = (sequelize, DataTypes) => { } }; Post.init({ - title: DataTypes.STRING, - content: DataTypes.STRING, + title: { + type: DataTypes.STRING, + allowNull: false, + validate: { + notNull: { + args: true, + msg: 'Title must not be null.' + }, + notEmpty: { + args: true, + msg: 'Title must not be empty.', + }, + }, + }, + content: { + type: DataTypes.STRING, + allowNull: false, + validate: { + notNull: { + args: true, + msg: 'Content must not be null.' + }, + notEmpty: { + args: true, + msg: 'Content must not be empty.', + }, + }, + }, UserId: DataTypes.INTEGER, }, { sequelize, diff --git a/server/services/users/src/models/user.js b/server/services/users/src/models/user.js index b5f9b8c..61dd157 100644 --- a/server/services/users/src/models/user.js +++ b/server/services/users/src/models/user.js @@ -24,6 +24,10 @@ module.exports = (sequelize, DataTypes) => { allowNull: false, unique: true, validate: { + notNull: { + args: true, + msg: 'Username must not be null.' + }, notEmpty: { args: true, msg: 'Username must not be empty.', @@ -45,6 +49,10 @@ module.exports = (sequelize, DataTypes) => { allowNull: false, unique: true, validate: { + notNull: { + args: true, + msg: 'Email must not be null.' + }, notEmpty: { args: true, msg: 'Email must not be empty.', @@ -69,6 +77,10 @@ module.exports = (sequelize, DataTypes) => { type: DataTypes.STRING, allowNull: false, validate: { + notNull: { + args: true, + msg: 'Password must not be null.' + }, notEmpty: { args: true, msg: 'Password must not be empty.' From f6996d6bb6e88b4e0b6a2bc2ec7b36e6f1418a5c Mon Sep 17 00:00:00 2001 From: Alfian Tirta Date: Wed, 25 Nov 2020 09:37:57 +0700 Subject: [PATCH 11/37] added authorization middleware, implemented authorization in edit and delete blog post feature --- server/services/users/api_doc.md | 14 +++++++++++++ .../users/src/helpers/errorHandler.js | 3 +++ .../users/src/middlewares/authentication.js | 5 ++--- .../users/src/middlewares/authorization.js | 21 +++++++++++++++++++ .../services/users/src/routes/posts/index.js | 2 ++ 5 files changed, 42 insertions(+), 3 deletions(-) create mode 100644 server/services/users/src/middlewares/authorization.js diff --git a/server/services/users/api_doc.md b/server/services/users/api_doc.md index c80af1f..406f458 100644 --- a/server/services/users/api_doc.md +++ b/server/services/users/api_doc.md @@ -462,6 +462,13 @@ _Response (401 - Unauthorized)_ ] ``` +_Response (403 - Forbidden)_ +``` +[ + "The user is not authorized." +] +``` + _Response (500 - Internal Server Error)_ ``` [ @@ -514,6 +521,13 @@ _Response (401 - Unauthorized)_ ] ``` +_Response (403 - Forbidden)_ +``` +[ + "The user is not authorized." +] +``` + _Response (500 - Internal Server Error)_ ``` [ diff --git a/server/services/users/src/helpers/errorHandler.js b/server/services/users/src/helpers/errorHandler.js index 65783ba..57bb55e 100644 --- a/server/services/users/src/helpers/errorHandler.js +++ b/server/services/users/src/helpers/errorHandler.js @@ -18,6 +18,9 @@ function errorHandler(err) { if (err.message === 'The user is not authenticated.') { status = 401; } + if (err.message === 'The user is not authorized.') { + status = 403; + } break; default : errors.push('Internal Server Error'); diff --git a/server/services/users/src/middlewares/authentication.js b/server/services/users/src/middlewares/authentication.js index 46b4ad2..65e01ea 100644 --- a/server/services/users/src/middlewares/authentication.js +++ b/server/services/users/src/middlewares/authentication.js @@ -6,10 +6,9 @@ async function authentication(ctx, next) { const { access_token } = ctx.request.header; try { const decoded_user_data = verify_jwt_token(access_token); + const { id, username, email, status } = decoded_user_data; const user = await User.findOne({ - where: { - email: decoded_user_data.email, - } + where: { id, username, email, status }, }); if (!user) { throw new Error('The user is not authenticated.'); diff --git a/server/services/users/src/middlewares/authorization.js b/server/services/users/src/middlewares/authorization.js new file mode 100644 index 0000000..fdee354 --- /dev/null +++ b/server/services/users/src/middlewares/authorization.js @@ -0,0 +1,21 @@ +const { User, Post } = require('../models'); +const { verify_jwt_token } = require('../helpers/jwt'); +const errorHandler = require('../helpers/errorHandler'); + +async function authorization(ctx, next) { + const id = ctx.request.params.id; + try { + const post = await Post.findByPk(id); + if (post && post.UserId === ctx.user.id) { + await next(); + } else { + throw new Error('The user is not authorized.'); + } + } catch(err) { + const { status, errors } = errorHandler(err); + ctx.response.status = status; + ctx.body = errors; + } +} + +module.exports = authorization; diff --git a/server/services/users/src/routes/posts/index.js b/server/services/users/src/routes/posts/index.js index 954b9d4..5a07cb5 100644 --- a/server/services/users/src/routes/posts/index.js +++ b/server/services/users/src/routes/posts/index.js @@ -1,5 +1,6 @@ const Controller = require('../../controllers/PostController'); const authentication = require('../../middlewares/authentication'); +const authorization = require('../../middlewares/authorization'); function postsRoute(router) { router @@ -8,6 +9,7 @@ function postsRoute(router) { .get('/posts', Controller.read) .get('/posts/:id', Controller.findByPostId) .get('/posts/user/:id', Controller.findByUserId) + .use(authorization) .put('/posts/:id', Controller.update) .delete('/posts/:id', Controller.delete); } From 2d2bdd5c941866fab88c2881c32ec98c45625856 Mon Sep 17 00:00:00 2001 From: Alfian Tirta Date: Wed, 25 Nov 2020 11:14:54 +0700 Subject: [PATCH 12/37] added likes endpoints along with constraints and validations; added authorization middleware for unlike feature --- server/services/users/api_doc.md | 129 +++++++++++++++++- .../users/src/controllers/LikeController.js | 52 +++++++ .../users/src/controllers/PostController.js | 5 +- .../users/src/middlewares/authorization.js | 31 ++++- server/services/users/src/models/like.js | 11 +- server/services/users/src/routes/index.js | 4 + .../services/users/src/routes/likes/index.js | 10 ++ .../services/users/src/routes/posts/index.js | 10 +- 8 files changed, 235 insertions(+), 17 deletions(-) create mode 100644 server/services/users/src/controllers/LikeController.js create mode 100644 server/services/users/src/routes/likes/index.js diff --git a/server/services/users/api_doc.md b/server/services/users/api_doc.md index 406f458..49fe43b 100644 --- a/server/services/users/api_doc.md +++ b/server/services/users/api_doc.md @@ -495,12 +495,15 @@ not needed _Response (200 - OK)_ ``` { - "id": , - "title": "", - "content": "", - "UserId": , - "createdAt": "", - "updatedAt": "" + "message": "Delete Success", + "deleted_post": { + "id": , + "title": "", + "content": "", + "UserId": , + "createdAt": "", + "updatedAt": "" + } } ``` @@ -534,3 +537,117 @@ _Response (500 - Internal Server Error)_ "Internal Server Error" ] ``` +--- +### POST /likes + +> Give a like to a blog post + +_Request Header_ +``` +{ + "access_token": "" +} +``` + +_Request Body_ +``` +{ + "PostId": +} +``` + +_Response (201 - Created)_ +``` +{ + "id": 1, + "PostId": "", + "UserId": , + "createdAt": "", + "updatedAt": "" +} +``` + +_Response (400 - Bad Request)_ +``` +[ + "", + "", + ..., + "" +] +``` + +_Response (401 - Unauthorized)_ +``` +[ + "The user is not authenticated." +] +``` + +_Response (500 - Internal Server Error)_ +``` +[ + "Internal Server Error" +] +``` +--- +### DELETE /likes/:id + +> Delete a like by its id + +_Request Header_ +``` +{ + "access_token": "" +} +``` + +_Request Body_ +``` +not needed +``` + +_Response (200 - OK)_ +``` +{ + "message": "Delete Success", + "deleted_like": { + "id": , + "PostId": "", + "UserId": , + "createdAt": "", + "updatedAt": "" + } +} +``` + +_Response (400 - Bad Request)_ +``` +[ + "", + "", + ..., + "" +] +``` + +_Response (401 - Unauthorized)_ +``` +[ + "The user is not authenticated." +] +``` + +_Response (403 - Forbidden)_ +``` +[ + "The user is not authorized." +] +``` + +_Response (500 - Internal Server Error)_ +``` +[ + "Internal Server Error" +] +``` \ No newline at end of file diff --git a/server/services/users/src/controllers/LikeController.js b/server/services/users/src/controllers/LikeController.js new file mode 100644 index 0000000..60761d3 --- /dev/null +++ b/server/services/users/src/controllers/LikeController.js @@ -0,0 +1,52 @@ +const { Like, Post, User } = require('../models'); +const errorHandler = require('../helpers/errorHandler'); + +class LikeController { + static async create(ctx) { + const UserId = ctx.user.id; + if (ctx.request.body.PostId === undefined) { + ctx.request.body.PostId = null; + } + const { PostId } = ctx.request.body; + try { + const like = await Like.findOne({ + where: { PostId, UserId } + }); + if (!like) { + const post = await Post.findByPk(PostId); + if (!post) { + throw new Error('The post does not exist.'); + } else { + const like = await Like.create({ PostId, UserId }); + ctx.response.status = 201; + ctx.body = like; + } + } else { + throw new Error('You cannot give more than one like for the same post.'); + } + } catch(err) { + const { status, errors } = errorHandler(err); + ctx.response.status = status; + ctx.body = errors; + } + } + + static async delete(ctx) { + const id = +ctx.request.params.id; + try { + const deleted_like = await Like.findByPk(id); + await Like.destroy({ where: { id } }); + ctx.response.status = 200; + ctx.body = { + message: 'Delete Success', + deleted_like, + }; + } catch(err) { + const { status, errors } = errorHandler(err); + ctx.response.status = status; + ctx.body = errors; + } + } +} + +module.exports = LikeController; diff --git a/server/services/users/src/controllers/PostController.js b/server/services/users/src/controllers/PostController.js index e33ada4..29d14a3 100644 --- a/server/services/users/src/controllers/PostController.js +++ b/server/services/users/src/controllers/PostController.js @@ -88,7 +88,10 @@ class PostController { const deleted_post = await Post.findByPk(id); await Post.destroy({ where: { id } }); ctx.response.status = 200; - ctx.body = deleted_post; + ctx.body = { + message: 'Delete Success', + deleted_post, + }; } catch(err) { const { status, errors } = errorHandler(err); ctx.response.status = status; diff --git a/server/services/users/src/middlewares/authorization.js b/server/services/users/src/middlewares/authorization.js index fdee354..0c60991 100644 --- a/server/services/users/src/middlewares/authorization.js +++ b/server/services/users/src/middlewares/authorization.js @@ -1,13 +1,16 @@ -const { User, Post } = require('../models'); +const { User, Post, Like } = require('../models'); const { verify_jwt_token } = require('../helpers/jwt'); const errorHandler = require('../helpers/errorHandler'); -async function authorization(ctx, next) { +// authorization check before updating or deletinga post : +async function authorization_post(ctx, next) { const id = ctx.request.params.id; try { const post = await Post.findByPk(id); if (post && post.UserId === ctx.user.id) { await next(); + } else if (!post) { + throw new Error('The post does not exist.'); } else { throw new Error('The user is not authorized.'); } @@ -18,4 +21,26 @@ async function authorization(ctx, next) { } } -module.exports = authorization; +// authorization check before deleting a like : +async function authorization_like(ctx, next) { + const id = ctx.request.params.id; + try { + const like = await Like.findByPk(id); + if (like && like.UserId === ctx.user.id) { + await next(); + } else if (!like) { + throw new Error('The like does not exist.'); + } else { + throw new Error('The user is not authorized.'); + } + } catch(err) { + const { status, errors } = errorHandler(err); + ctx.response.status = status; + ctx.body = errors; + } +} + +module.exports = { + authorization_post, + authorization_like, +}; diff --git a/server/services/users/src/models/like.js b/server/services/users/src/models/like.js index a122480..46b3309 100644 --- a/server/services/users/src/models/like.js +++ b/server/services/users/src/models/like.js @@ -16,7 +16,16 @@ module.exports = (sequelize, DataTypes) => { } }; Like.init({ - PostId: DataTypes.INTEGER, + PostId: { + type: DataTypes.INTEGER, + allowNull: false, + validate: { + notNull: { + args: true, + msg: 'PostId must not be null.', + }, + }, + }, UserId: DataTypes.INTEGER, }, { sequelize, diff --git a/server/services/users/src/routes/index.js b/server/services/users/src/routes/index.js index 2fd55de..2a35657 100644 --- a/server/services/users/src/routes/index.js +++ b/server/services/users/src/routes/index.js @@ -1,9 +1,13 @@ const usersRoute = require('./users'); const postsRoute = require('./posts'); +const likesRoute = require('./likes'); +const authentication = require('../middlewares/authentication'); function loadRoutes(router) { usersRoute(router); + router.use(authentication); postsRoute(router); + likesRoute(router); }; module.exports = loadRoutes; diff --git a/server/services/users/src/routes/likes/index.js b/server/services/users/src/routes/likes/index.js new file mode 100644 index 0000000..d88e9de --- /dev/null +++ b/server/services/users/src/routes/likes/index.js @@ -0,0 +1,10 @@ +const Controller = require('../../controllers/LikeController'); +const { authorization_like } = require('../../middlewares/authorization'); + +function likesRoute(router) { + router + .post('/likes', Controller.create) + .delete('/likes/:id', authorization_like, Controller.delete); +} + +module.exports = likesRoute; diff --git a/server/services/users/src/routes/posts/index.js b/server/services/users/src/routes/posts/index.js index 5a07cb5..350f0a5 100644 --- a/server/services/users/src/routes/posts/index.js +++ b/server/services/users/src/routes/posts/index.js @@ -1,17 +1,15 @@ const Controller = require('../../controllers/PostController'); -const authentication = require('../../middlewares/authentication'); -const authorization = require('../../middlewares/authorization'); +const { authorization_post } = require('../../middlewares/authorization'); function postsRoute(router) { router - .use(authentication) .post('/posts', Controller.create) .get('/posts', Controller.read) .get('/posts/:id', Controller.findByPostId) .get('/posts/user/:id', Controller.findByUserId) - .use(authorization) - .put('/posts/:id', Controller.update) - .delete('/posts/:id', Controller.delete); + // .use(authorization_post) + .put('/posts/:id', authorization_post, Controller.update) + .delete('/posts/:id', authorization_post, Controller.delete); } module.exports = postsRoute; From 7963265418ed0b9b8c475d46c4b01589e4330c01 Mon Sep 17 00:00:00 2001 From: Alfian Tirta Date: Wed, 25 Nov 2020 15:26:15 +0700 Subject: [PATCH 13/37] opened http get methods to public (including unregistered user) --- .../src/controllers/CommentController.js | 10 +++++++++ .../users/src/middlewares/authorization.js | 22 ++++++++++++++++++- .../users/src/routes/comments/index.js | 11 ++++++++++ server/services/users/src/routes/index.js | 4 ++-- .../services/users/src/routes/likes/index.js | 5 +++-- .../services/users/src/routes/posts/index.js | 8 +++---- 6 files changed, 51 insertions(+), 9 deletions(-) create mode 100644 server/services/users/src/controllers/CommentController.js create mode 100644 server/services/users/src/routes/comments/index.js diff --git a/server/services/users/src/controllers/CommentController.js b/server/services/users/src/controllers/CommentController.js new file mode 100644 index 0000000..1107684 --- /dev/null +++ b/server/services/users/src/controllers/CommentController.js @@ -0,0 +1,10 @@ +const { Comment } = require('../models'); +const errorHandler = require('../helpers/errorHandler'); + +class CommentController { + static async create(ctx) {} + + static async delete(ctx) {} +} + +module.exports = CommentController; diff --git a/server/services/users/src/middlewares/authorization.js b/server/services/users/src/middlewares/authorization.js index 0c60991..b61f161 100644 --- a/server/services/users/src/middlewares/authorization.js +++ b/server/services/users/src/middlewares/authorization.js @@ -1,4 +1,4 @@ -const { User, Post, Like } = require('../models'); +const { User, Post, Like, Comment, SubComment } = require('../models'); const { verify_jwt_token } = require('../helpers/jwt'); const errorHandler = require('../helpers/errorHandler'); @@ -40,7 +40,27 @@ async function authorization_like(ctx, next) { } } +// authorization check before deleting a comment : +async function authorization_comment(ctx, next) { + const id = ctx.request.params.id; + try { + const comment = await Comment.findByPk(id); + if (comment && comment.UserId === ctx.user.id) { + await next(); + } else if (!comment) { + throw new Error('The comment does not exist.'); + } else { + throw new Error('The user is not authorized.'); + } + } catch(err) { + const { status, errors } = errorHandler(err); + ctx.response.status = status; + ctx.body = errors; + } +} + module.exports = { authorization_post, authorization_like, + authorization_comment, }; diff --git a/server/services/users/src/routes/comments/index.js b/server/services/users/src/routes/comments/index.js new file mode 100644 index 0000000..96573ea --- /dev/null +++ b/server/services/users/src/routes/comments/index.js @@ -0,0 +1,11 @@ +const Controller = require('../../controllers/CommentController'); +const authentication = require('../../middlewares/authentication'); +const { authorization_comment } = require('../../middlewares/authorization'); + +function commentsRoute(router) { + router + .post('/comments', authentication, Controller.create) + .delete('/comments', authentication, authorization_comment, Controller.delete); +} + +module.exports = commentsRoute; diff --git a/server/services/users/src/routes/index.js b/server/services/users/src/routes/index.js index 2a35657..83204a9 100644 --- a/server/services/users/src/routes/index.js +++ b/server/services/users/src/routes/index.js @@ -1,13 +1,13 @@ const usersRoute = require('./users'); const postsRoute = require('./posts'); const likesRoute = require('./likes'); -const authentication = require('../middlewares/authentication'); +const commentsRoute = require('./comments'); function loadRoutes(router) { usersRoute(router); - router.use(authentication); postsRoute(router); likesRoute(router); + commentsRoute(router); }; module.exports = loadRoutes; diff --git a/server/services/users/src/routes/likes/index.js b/server/services/users/src/routes/likes/index.js index d88e9de..0fadacb 100644 --- a/server/services/users/src/routes/likes/index.js +++ b/server/services/users/src/routes/likes/index.js @@ -1,10 +1,11 @@ const Controller = require('../../controllers/LikeController'); +const authentication = require('../../middlewares/authentication'); const { authorization_like } = require('../../middlewares/authorization'); function likesRoute(router) { router - .post('/likes', Controller.create) - .delete('/likes/:id', authorization_like, Controller.delete); + .post('/likes', authentication, Controller.create) + .delete('/likes/:id', authentication, authorization_like, Controller.delete); } module.exports = likesRoute; diff --git a/server/services/users/src/routes/posts/index.js b/server/services/users/src/routes/posts/index.js index 350f0a5..959532b 100644 --- a/server/services/users/src/routes/posts/index.js +++ b/server/services/users/src/routes/posts/index.js @@ -1,15 +1,15 @@ const Controller = require('../../controllers/PostController'); +const authentication = require('../../middlewares/authentication'); const { authorization_post } = require('../../middlewares/authorization'); function postsRoute(router) { router - .post('/posts', Controller.create) + .post('/posts', authentication, Controller.create) .get('/posts', Controller.read) .get('/posts/:id', Controller.findByPostId) .get('/posts/user/:id', Controller.findByUserId) - // .use(authorization_post) - .put('/posts/:id', authorization_post, Controller.update) - .delete('/posts/:id', authorization_post, Controller.delete); + .put('/posts/:id', authentication, authorization_post, Controller.update) + .delete('/posts/:id', authentication, authorization_post, Controller.delete); } module.exports = postsRoute; From 8c89d99d4995c6082768a547a6de52553f155a6c Mon Sep 17 00:00:00 2001 From: Alfian Tirta Date: Wed, 25 Nov 2020 16:19:27 +0700 Subject: [PATCH 14/37] added comments endpoints with validation --- server/services/users/api_doc.md | 145 ++++++++++++++++-- .../src/controllers/CommentController.js | 44 +++++- .../users/src/controllers/LikeController.js | 8 +- .../users/src/controllers/PostController.js | 32 ++-- .../users/src/controllers/UserController.js | 12 +- .../users/src/middlewares/authentication.js | 2 +- .../users/src/middlewares/authorization.js | 6 +- server/services/users/src/models/comment.js | 28 +++- .../users/src/routes/comments/index.js | 2 +- 9 files changed, 236 insertions(+), 43 deletions(-) diff --git a/server/services/users/api_doc.md b/server/services/users/api_doc.md index 49fe43b..24cac46 100644 --- a/server/services/users/api_doc.md +++ b/server/services/users/api_doc.md @@ -224,9 +224,7 @@ _Response (500 - Internal Server Error)_ _Request Header_ ``` -{ - "access_token": "" -} +not needed ``` _Request Body_ @@ -291,9 +289,7 @@ _Response (500 - Internal Server Error)_ _Request Header_ ``` -{ - "access_token": "" -} +not needed ``` _Request Body_ @@ -354,9 +350,7 @@ _Response (500 - Internal Server Error)_ _Request Header_ ``` -{ - "access_token": "" -} +not needed ``` _Request Body_ @@ -531,6 +525,130 @@ _Response (403 - Forbidden)_ ] ``` +_Response (500 - Internal Server Error)_ +``` +[ + "Internal Server Error" +] +``` +--- +### POST /comments + +> Give a comment to a blog post + +_Request Header_ +``` +{ + "access_token": "" +} +``` + +_Request Body_ +``` +{ + "content": "", + "PostId": +} +``` + +_Response (201 - Created)_ +``` +{ + "id": , + "content": "", + "PostId": "", + "UserId": , + "createdAt": "", + "updatedAt": "" +} +``` + +_Response (400 - Bad Request)_ +``` +[ + "", + "", + ..., + "" +] +``` + +_Response (401 - Unauthorized)_ +``` +[ + "The user is not authenticated." +] +``` + +_Response (403 - Forbidden)_ +``` +[ + "The user is not authorized." +] +``` + +_Response (500 - Internal Server Error)_ +``` +[ + "Internal Server Error" +] +``` +--- +### DELETE /comments/:id + +> Delete a comment by its id + +_Request Header_ +``` +{ + "access_token": "" +} +``` + +_Request Body_ +``` +not needed +``` + +_Response (200 - OK)_ +``` +{ + "message": "Delete Success", + "deleted_comment": { + "id": , + "content": "", + "PostId": "", + "UserId": , + "createdAt": "", + "updatedAt": "" + } +} +``` + +_Response (400 - Bad Request)_ +``` +[ + "", + "", + ..., + "" +] +``` + +_Response (401 - Unauthorized)_ +``` +[ + "The user is not authenticated." +] +``` + +_Response (403 - Forbidden)_ +``` +[ + "The user is not authorized." +] +``` + _Response (500 - Internal Server Error)_ ``` [ @@ -559,7 +677,7 @@ _Request Body_ _Response (201 - Created)_ ``` { - "id": 1, + "id": , "PostId": "", "UserId": , "createdAt": "", @@ -584,6 +702,13 @@ _Response (401 - Unauthorized)_ ] ``` +_Response (403 - Forbidden)_ +``` +[ + "The user is not authorized." +] +``` + _Response (500 - Internal Server Error)_ ``` [ diff --git a/server/services/users/src/controllers/CommentController.js b/server/services/users/src/controllers/CommentController.js index 1107684..0547b6c 100644 --- a/server/services/users/src/controllers/CommentController.js +++ b/server/services/users/src/controllers/CommentController.js @@ -1,10 +1,48 @@ -const { Comment } = require('../models'); +const { Comment, Post, User } = require('../models'); const errorHandler = require('../helpers/errorHandler'); class CommentController { - static async create(ctx) {} + static async create(ctx) { + const UserId = ctx.user.id; + if (ctx.request.body.content === undefined) { + ctx.request.body.content = null; + } + if (ctx.request.body.PostId === undefined) { + ctx.request.body.PostId = null; + } + const { content, PostId } = ctx.request.body; + try { + const post = await Post.findByPk(PostId); + if (!post && content && PostId) { + throw new Error('The post does not exist.'); + } else { + const comment = await Comment.create({ content, PostId, UserId }); + ctx.response.status = 201; + ctx.response.body = comment; + } + } catch(err) { + const { status, errors } = errorHandler(err); + ctx.response.status = status; + ctx.response.body = errors; + } + } - static async delete(ctx) {} + static async delete(ctx) { + const id = +ctx.request.params.id; + try { + const deleted_comment = await Comment.findByPk(id); + await Comment.destroy({ where: { id }}); + ctx.response.status = 200; + ctx.response.body = { + message: 'Delete Success', + deleted_comment, + }; + } catch(err) { + const { status, errors } = errorHandler(err); + ctx.response.status = status; + ctx.response.body = errors; + } + } } module.exports = CommentController; diff --git a/server/services/users/src/controllers/LikeController.js b/server/services/users/src/controllers/LikeController.js index 60761d3..0e1408f 100644 --- a/server/services/users/src/controllers/LikeController.js +++ b/server/services/users/src/controllers/LikeController.js @@ -19,7 +19,7 @@ class LikeController { } else { const like = await Like.create({ PostId, UserId }); ctx.response.status = 201; - ctx.body = like; + ctx.response.body = like; } } else { throw new Error('You cannot give more than one like for the same post.'); @@ -27,7 +27,7 @@ class LikeController { } catch(err) { const { status, errors } = errorHandler(err); ctx.response.status = status; - ctx.body = errors; + ctx.response.body = errors; } } @@ -37,14 +37,14 @@ class LikeController { const deleted_like = await Like.findByPk(id); await Like.destroy({ where: { id } }); ctx.response.status = 200; - ctx.body = { + ctx.response.body = { message: 'Delete Success', deleted_like, }; } catch(err) { const { status, errors } = errorHandler(err); ctx.response.status = status; - ctx.body = errors; + ctx.response.body = errors; } } } diff --git a/server/services/users/src/controllers/PostController.js b/server/services/users/src/controllers/PostController.js index 29d14a3..599fe1c 100644 --- a/server/services/users/src/controllers/PostController.js +++ b/server/services/users/src/controllers/PostController.js @@ -3,16 +3,22 @@ const errorHandler = require('../helpers/errorHandler'); class PostController { static async create(ctx) { - const { title, content } = ctx.request.body; const UserId = ctx.user.id; + if (ctx.request.body.title === undefined) { + ctx.request.body.title = null; + } + if (ctx.request.body.content === undefined) { + ctx.request.body.content = null; + } + const { title, content } = ctx.request.body; try { const new_post = await Post.create({ title, content, UserId }); ctx.response.status = 201; - ctx.body = new_post; + ctx.response.body = new_post; } catch(err) { const { status, errors } = errorHandler(err); ctx.response.status = status; - ctx.body = errors; + ctx.response.body = errors; } } @@ -20,11 +26,11 @@ class PostController { try { const all_posts = await Post.findAll({ include: [User, Like, Comment] }); ctx.response.status = 200; - ctx.body = all_posts; + ctx.response.body = all_posts; } catch(err) { const { status, errors } = errorHandler(err); ctx.response.status = status; - ctx.body = errors; + ctx.response.body = errors; } } @@ -35,11 +41,11 @@ class PostController { include: [User, Like, Comment], }); ctx.response.status = 200; - ctx.body = posts; + ctx.response.body = posts; } catch(err) { const { status, errors } = errorHandler(err); ctx.response.status = status; - ctx.body = errors; + ctx.response.body = errors; } } @@ -51,11 +57,11 @@ class PostController { include: [User, Like, Comment], }); ctx.response.status = 200; - ctx.body = posts; + ctx.response.body = posts; } catch(err) { const { status, errors } = errorHandler(err); ctx.response.status = status; - ctx.body = errors; + ctx.response.body = errors; } } @@ -74,11 +80,11 @@ class PostController { returning: true, }); ctx.response.status = 200; - ctx.body = updated_post[1][0]; + ctx.response.body = updated_post[1][0]; } catch(err) { const { status, errors } = errorHandler(err); ctx.response.status = status; - ctx.body = errors; + ctx.response.body = errors; } } @@ -88,14 +94,14 @@ class PostController { const deleted_post = await Post.findByPk(id); await Post.destroy({ where: { id } }); ctx.response.status = 200; - ctx.body = { + ctx.response.body = { message: 'Delete Success', deleted_post, }; } catch(err) { const { status, errors } = errorHandler(err); ctx.response.status = status; - ctx.body = errors; + ctx.response.body = errors; } } } diff --git a/server/services/users/src/controllers/UserController.js b/server/services/users/src/controllers/UserController.js index 5cfef09..9eb28cf 100644 --- a/server/services/users/src/controllers/UserController.js +++ b/server/services/users/src/controllers/UserController.js @@ -10,7 +10,7 @@ class UserController { try { const new_user = await User.create({ username, email, password }); ctx.response.status = 201; - ctx.body = new_user; + ctx.response.body = new_user; // Generate user verification token : const verification_token = generate_jwt_token(new_user); @@ -27,7 +27,7 @@ class UserController { } catch(err) { const { status, errors } = errorHandler(err); ctx.response.status = status; - ctx.body = errors; + ctx.response.body = errors; } } @@ -53,12 +53,12 @@ class UserController { returning: true, }); ctx.response.status = 200; - ctx.body = { message: 'User Verification Success' }; + ctx.response.body = { message: 'User Verification Success' }; } } catch(err) { const { status, errors } = errorHandler(err); ctx.response.status = status; - ctx.body = errors; + ctx.response.body = errors; } } @@ -79,13 +79,13 @@ class UserController { } else { const access_token = generate_jwt_token(user); ctx.response.status = 200; - ctx.body = { access_token, user_id: user.id }; + ctx.response.body = { access_token, user_id: user.id }; } } } catch(err) { const { status, errors } = errorHandler(err); ctx.response.status = status; - ctx.body = errors; + ctx.response.body = errors; } } } diff --git a/server/services/users/src/middlewares/authentication.js b/server/services/users/src/middlewares/authentication.js index 65e01ea..e3165a8 100644 --- a/server/services/users/src/middlewares/authentication.js +++ b/server/services/users/src/middlewares/authentication.js @@ -18,7 +18,7 @@ async function authentication(ctx, next) { } catch(err) { const { status, errors } = errorHandler(err); ctx.response.status = status; - ctx.body = errors; + ctx.response.body = errors; } } diff --git a/server/services/users/src/middlewares/authorization.js b/server/services/users/src/middlewares/authorization.js index b61f161..3eb71b7 100644 --- a/server/services/users/src/middlewares/authorization.js +++ b/server/services/users/src/middlewares/authorization.js @@ -17,7 +17,7 @@ async function authorization_post(ctx, next) { } catch(err) { const { status, errors } = errorHandler(err); ctx.response.status = status; - ctx.body = errors; + ctx.response.body = errors; } } @@ -36,7 +36,7 @@ async function authorization_like(ctx, next) { } catch(err) { const { status, errors } = errorHandler(err); ctx.response.status = status; - ctx.body = errors; + ctx.response.body = errors; } } @@ -55,7 +55,7 @@ async function authorization_comment(ctx, next) { } catch(err) { const { status, errors } = errorHandler(err); ctx.response.status = status; - ctx.body = errors; + ctx.response.body = errors; } } diff --git a/server/services/users/src/models/comment.js b/server/services/users/src/models/comment.js index 8229e7e..5e3a349 100644 --- a/server/services/users/src/models/comment.js +++ b/server/services/users/src/models/comment.js @@ -17,8 +17,32 @@ module.exports = (sequelize, DataTypes) => { } }; Comment.init({ - content: DataTypes.STRING, - PostId: DataTypes.INTEGER, + content: { + type: DataTypes.STRING, + allowNull: false, + validate: { + notNull: { + args: true, + msg: 'Content must not be null.', + }, + notEmpty: { + args: true, + msg: 'Content must not be empty.', + }, + }, + }, + PostId: { + type: DataTypes.INTEGER, + allowNull: false, + validate: { + notNull: { + notNull: { + args: true, + msg: 'PostId must not be null.', + }, + }, + }, + }, UserId: DataTypes.INTEGER, }, { sequelize, diff --git a/server/services/users/src/routes/comments/index.js b/server/services/users/src/routes/comments/index.js index 96573ea..cfa12d1 100644 --- a/server/services/users/src/routes/comments/index.js +++ b/server/services/users/src/routes/comments/index.js @@ -5,7 +5,7 @@ const { authorization_comment } = require('../../middlewares/authorization'); function commentsRoute(router) { router .post('/comments', authentication, Controller.create) - .delete('/comments', authentication, authorization_comment, Controller.delete); + .delete('/comments/:id', authentication, authorization_comment, Controller.delete); } module.exports = commentsRoute; From 4f66270e1bbfa96aa01c58872da0cce49f88a096 Mon Sep 17 00:00:00 2001 From: Alfian Tirta Date: Wed, 25 Nov 2020 17:33:51 +0700 Subject: [PATCH 15/37] added sub-comments endpoints, updated get users, updated users service api doc --- server/services/users/api_doc.md | 228 +++++++++++++++++- .../users/src/controllers/PostController.js | 34 ++- .../src/controllers/SubCommentController.js | 48 ++++ .../users/src/middlewares/authorization.js | 26 +- server/services/users/src/models/comment.js | 6 +- .../services/users/src/models/subcomment.js | 26 +- server/services/users/src/routes/index.js | 2 + .../users/src/routes/sub-comments/index.js | 11 + 8 files changed, 361 insertions(+), 20 deletions(-) create mode 100644 server/services/users/src/controllers/SubCommentController.js create mode 100644 server/services/users/src/routes/sub-comments/index.js diff --git a/server/services/users/api_doc.md b/server/services/users/api_doc.md index 24cac46..32a5068 100644 --- a/server/services/users/api_doc.md +++ b/server/services/users/api_doc.md @@ -251,8 +251,38 @@ _Response (200 - OK)_ "createdAt": "", "updatedAt": "" }, - "Likes": [array of post likes], - "Comments": [array of post comments] + "Likes": [ + { + "id": , + "PostId": , + "UserId": , + "createdAt": "", + "updatedAt": "" + }, + ... + ], + "Comments": [ + { + "id": , + "content": "", + "PostId": , + "UserId": , + "createdAt": "", + "updatedAt": "", + "SubComments": [ + { + "id": , + "content": "", + "CommentId": , + "UserId": , + "createdAt": "", + "updatedAt": "" + }, + ... + ] + }, + ... + ] }, ... ] @@ -315,8 +345,38 @@ _Response (200 - OK)_ "createdAt": "", "updatedAt": "" }, - "Likes": [array of post likes], - "Comments": [array of post comments] + "Likes": [ + { + "id": , + "PostId": , + "UserId": , + "createdAt": "", + "updatedAt": "" + }, + ... + ], + "Comments": [ + { + "id": , + "content": "", + "PostId": , + "UserId": , + "createdAt": "", + "updatedAt": "", + "SubComments": [ + { + "id": , + "content": "", + "CommentId": , + "UserId": , + "createdAt": "", + "updatedAt": "" + }, + ... + ] + }, + ... + ] } ``` @@ -377,8 +437,38 @@ _Response (200 - OK)_ "createdAt": "", "updatedAt": "" }, - "Likes": [array of post likes], - "Comments": [array of post comments] + "Likes": [ + { + "id": , + "PostId": , + "UserId": , + "createdAt": "", + "updatedAt": "" + }, + ... + ], + "Comments": [ + { + "id": , + "content": "", + "PostId": , + "UserId": , + "createdAt": "", + "updatedAt": "", + "SubComments": [ + { + "id": , + "content": "", + "CommentId": , + "UserId": , + "createdAt": "", + "updatedAt": "" + }, + ... + ] + }, + ... + ] }, ... ] @@ -534,7 +624,7 @@ _Response (500 - Internal Server Error)_ --- ### POST /comments -> Give a comment to a blog post +> Write a comment to a blog post _Request Header_ ``` @@ -649,6 +739,130 @@ _Response (403 - Forbidden)_ ] ``` +_Response (500 - Internal Server Error)_ +``` +[ + "Internal Server Error" +] +``` +--- +### POST /sub-comments + +> Write a sub comment to a comment in a blog post + +_Request Header_ +``` +{ + "access_token": "" +} +``` + +_Request Body_ +``` +{ + "content": "", + "CommentId": +} +``` + +_Response (201 - Created)_ +``` +{ + "id": , + "content": "", + "CommentId": "", + "UserId": , + "createdAt": "", + "updatedAt": "" +} +``` + +_Response (400 - Bad Request)_ +``` +[ + "", + "", + ..., + "" +] +``` + +_Response (401 - Unauthorized)_ +``` +[ + "The user is not authenticated." +] +``` + +_Response (403 - Forbidden)_ +``` +[ + "The user is not authorized." +] +``` + +_Response (500 - Internal Server Error)_ +``` +[ + "Internal Server Error" +] +``` +--- +### DELETE /sub-comments/:id + +> Delete a comment by its id + +_Request Header_ +``` +{ + "access_token": "" +} +``` + +_Request Body_ +``` +not needed +``` + +_Response (200 - OK)_ +``` +{ + "message": "Delete Success", + "deleted_sub_comment": { + "id": , + "content": "", + "CommentId": "", + "UserId": , + "createdAt": "", + "updatedAt": "" + } +} +``` + +_Response (400 - Bad Request)_ +``` +[ + "", + "", + ..., + "" +] +``` + +_Response (401 - Unauthorized)_ +``` +[ + "The user is not authenticated." +] +``` + +_Response (403 - Forbidden)_ +``` +[ + "The user is not authorized." +] +``` + _Response (500 - Internal Server Error)_ ``` [ diff --git a/server/services/users/src/controllers/PostController.js b/server/services/users/src/controllers/PostController.js index 599fe1c..38472ef 100644 --- a/server/services/users/src/controllers/PostController.js +++ b/server/services/users/src/controllers/PostController.js @@ -1,4 +1,4 @@ -const { Post, User, Like, Comment } = require('../models'); +const { Post, User, Like, Comment, SubComment } = require('../models'); const errorHandler = require('../helpers/errorHandler'); class PostController { @@ -24,7 +24,17 @@ class PostController { static async read(ctx) { try { - const all_posts = await Post.findAll({ include: [User, Like, Comment] }); + const all_posts = await Post.findAll({ + include: [{ + model: User, + }, + { + model: Like, + },{ + model: Comment, + include: [SubComment], + }], + }); ctx.response.status = 200; ctx.response.body = all_posts; } catch(err) { @@ -38,7 +48,15 @@ class PostController { const id = +ctx.request.params.id; try { const posts = await Post.findByPk(id, { - include: [User, Like, Comment], + include: [{ + model: User, + }, + { + model: Like, + },{ + model: Comment, + include: [SubComment], + }], }); ctx.response.status = 200; ctx.response.body = posts; @@ -54,7 +72,15 @@ class PostController { try { const posts = await Post.findAll({ where: { UserId: id }, - include: [User, Like, Comment], + include: [{ + model: User, + }, + { + model: Like, + },{ + model: Comment, + include: [SubComment], + }], }); ctx.response.status = 200; ctx.response.body = posts; diff --git a/server/services/users/src/controllers/SubCommentController.js b/server/services/users/src/controllers/SubCommentController.js new file mode 100644 index 0000000..b857778 --- /dev/null +++ b/server/services/users/src/controllers/SubCommentController.js @@ -0,0 +1,48 @@ +const { SubComment, Comment, User } = require('../models'); +const errorHandler = require('../helpers/errorHandler'); + +class SubCommentController { + static async create(ctx) { + const UserId = ctx.user.id; + if (ctx.request.body.content === undefined) { + ctx.request.body.content = null; + } + if (ctx.request.body.CommentId === undefined) { + ctx.request.body.CommentId = null; + } + const { content, CommentId } = ctx.request.body; + try { + const comment = await Comment.findByPk(CommentId); + if (!comment && content && PostId) { + throw new Error('The comment does not exist.'); + } else { + const sub_comment = await SubComment.create({ content, CommentId, UserId }); + ctx.response.status = 201; + ctx.response.body = sub_comment; + } + } catch(err) { + const { status, errors } = errorHandler(err); + ctx.response.status = status; + ctx.response.body = errors; + } + } + + static async delete(ctx) { + const id = +ctx.request.params.id; + try { + const deleted_sub_comment = await SubComment.findByPk(id); + await SubComment.destroy({ where: { id }}); + ctx.response.status = 200; + ctx.response.body = { + message: 'Delete Success', + deleted_sub_comment, + }; + } catch(err) { + const { status, errors } = errorHandler(err); + ctx.response.status = status; + ctx.response.body = errors; + } + } +} + +module.exports = SubCommentController; diff --git a/server/services/users/src/middlewares/authorization.js b/server/services/users/src/middlewares/authorization.js index 3eb71b7..1d99af4 100644 --- a/server/services/users/src/middlewares/authorization.js +++ b/server/services/users/src/middlewares/authorization.js @@ -4,7 +4,7 @@ const errorHandler = require('../helpers/errorHandler'); // authorization check before updating or deletinga post : async function authorization_post(ctx, next) { - const id = ctx.request.params.id; + const id = +ctx.request.params.id; try { const post = await Post.findByPk(id); if (post && post.UserId === ctx.user.id) { @@ -23,7 +23,7 @@ async function authorization_post(ctx, next) { // authorization check before deleting a like : async function authorization_like(ctx, next) { - const id = ctx.request.params.id; + const id = +ctx.request.params.id; try { const like = await Like.findByPk(id); if (like && like.UserId === ctx.user.id) { @@ -42,7 +42,7 @@ async function authorization_like(ctx, next) { // authorization check before deleting a comment : async function authorization_comment(ctx, next) { - const id = ctx.request.params.id; + const id = +ctx.request.params.id; try { const comment = await Comment.findByPk(id); if (comment && comment.UserId === ctx.user.id) { @@ -59,8 +59,28 @@ async function authorization_comment(ctx, next) { } } +// authorization check before deleting a sub comment : +async function authorization_sub_comment(ctx, next) { + const id = +ctx.request.params.id; console.log({id}); + try { + const sub_comment = await SubComment.findByPk(id); + if (sub_comment && sub_comment.UserId === ctx.user.id) { + await next(); + } else if (!sub_comment) { + throw new Error('The sub comment does not exist.'); + } else { + throw new Error('The user is not authorized.'); + } + } catch(err) { + const { status, errors } = errorHandler(err); + ctx.response.status = status; + ctx.response.body = errors; + } +} + module.exports = { authorization_post, authorization_like, authorization_comment, + authorization_sub_comment, }; diff --git a/server/services/users/src/models/comment.js b/server/services/users/src/models/comment.js index 5e3a349..1b792e8 100644 --- a/server/services/users/src/models/comment.js +++ b/server/services/users/src/models/comment.js @@ -36,10 +36,8 @@ module.exports = (sequelize, DataTypes) => { allowNull: false, validate: { notNull: { - notNull: { - args: true, - msg: 'PostId must not be null.', - }, + args: true, + msg: 'PostId must not be null.', }, }, }, diff --git a/server/services/users/src/models/subcomment.js b/server/services/users/src/models/subcomment.js index d87f776..6a26b0a 100644 --- a/server/services/users/src/models/subcomment.js +++ b/server/services/users/src/models/subcomment.js @@ -16,8 +16,30 @@ module.exports = (sequelize, DataTypes) => { } }; SubComment.init({ - content: DataTypes.STRING, - CommentId: DataTypes.INTEGER, + content: { + type: DataTypes.STRING, + allowNull: false, + validate: { + notNull: { + args: true, + msg: 'Content must not be null.', + }, + notEmpty: { + args: true, + msg: 'Content must not be empty.', + }, + }, + }, + CommentId: { + type: DataTypes.INTEGER, + allowNull: false, + validate: { + notNull: { + args: true, + msg: 'CommentId must not be null.', + }, + }, + }, UserId: DataTypes.INTEGER, }, { sequelize, diff --git a/server/services/users/src/routes/index.js b/server/services/users/src/routes/index.js index 83204a9..1484bdc 100644 --- a/server/services/users/src/routes/index.js +++ b/server/services/users/src/routes/index.js @@ -2,12 +2,14 @@ const usersRoute = require('./users'); const postsRoute = require('./posts'); const likesRoute = require('./likes'); const commentsRoute = require('./comments'); +const subCommentsRoute = require('./sub-comments'); function loadRoutes(router) { usersRoute(router); postsRoute(router); likesRoute(router); commentsRoute(router); + subCommentsRoute(router); }; module.exports = loadRoutes; diff --git a/server/services/users/src/routes/sub-comments/index.js b/server/services/users/src/routes/sub-comments/index.js new file mode 100644 index 0000000..fa069f5 --- /dev/null +++ b/server/services/users/src/routes/sub-comments/index.js @@ -0,0 +1,11 @@ +const Controller = require('../../controllers/SubCommentController'); +const authentication = require('../../middlewares/authentication'); +const { authorization_sub_comment } = require('../../middlewares/authorization'); + +function commentsRoute(router) { + router + .post('/sub-comments', authentication, Controller.create) + .delete('/sub-comments/:id', authentication, authorization_sub_comment, Controller.delete); +} + +module.exports = commentsRoute; From 8bd0cd41a6caeeded563eeef74e9f1d4640dab38 Mon Sep 17 00:00:00 2001 From: Alfian Tirta Date: Wed, 25 Nov 2020 23:07:53 +0700 Subject: [PATCH 16/37] updated erd, changed name db_schema to blog_app_erd --- blog_app_erd.png | Bin 0 -> 47550 bytes db_schema.png | Bin 46555 -> 0 bytes 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 blog_app_erd.png delete mode 100644 db_schema.png diff --git a/blog_app_erd.png b/blog_app_erd.png new file mode 100644 index 0000000000000000000000000000000000000000..b982d8a9f36527802e8c5ffeacb0ddc2da70a179 GIT binary patch literal 47550 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%0A&YwMW$l{OVsogM| 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 Vzfhx>mn8h7r#)wS)Ktrz{}&NzL+Stk literal 0 HcmV?d00001 diff --git a/db_schema.png b/db_schema.png deleted file mode 100644 index 421be55a086ca4177436915193e82bd0b282991b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 46555 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 G^Zx-(sVs8< From 839edad7d68cdc15e1d51ca501b9e243e2d9a1b9 Mon Sep 17 00:00:00 2001 From: Alfian Tirta Date: Wed, 25 Nov 2020 23:44:45 +0700 Subject: [PATCH 17/37] added send notification email when someone gave like or commented on a blog post --- server/services/users/api_doc.md | 2 +- .../users/src/controllers/CommentController.js | 12 +++++++++++- .../services/users/src/controllers/LikeController.js | 12 +++++++++++- .../services/users/src/controllers/UserController.js | 2 +- 4 files changed, 24 insertions(+), 4 deletions(-) diff --git a/server/services/users/api_doc.md b/server/services/users/api_doc.md index 32a5068..a169b88 100644 --- a/server/services/users/api_doc.md +++ b/server/services/users/api_doc.md @@ -1,4 +1,4 @@ -# Blog App - Users Service +# Blog App - Users Service API Blog App is an application to manage your blog. It has two services : Users & Logs. Its Users service has : diff --git a/server/services/users/src/controllers/CommentController.js b/server/services/users/src/controllers/CommentController.js index 0547b6c..8d0e1e3 100644 --- a/server/services/users/src/controllers/CommentController.js +++ b/server/services/users/src/controllers/CommentController.js @@ -1,5 +1,6 @@ const { Comment, Post, User } = require('../models'); const errorHandler = require('../helpers/errorHandler'); +const sendEmail = require('../helpers/mailgun'); class CommentController { static async create(ctx) { @@ -12,13 +13,22 @@ class CommentController { } const { content, PostId } = ctx.request.body; try { - const post = await Post.findByPk(PostId); + const post = await Post.findByPk(PostId, { include: [User] }); if (!post && content && PostId) { throw new Error('The post does not exist.'); } else { const comment = await Comment.create({ content, PostId, UserId }); ctx.response.status = 201; ctx.response.body = comment; + + // Send email with Mailgun API : + const email_data = { + from: `Blog App Team `, + to: `${post.User.email}`, + subject: `Blog App - Notification`, + text: `Hello, ${post.User.username}. Someone just commented on your post entitled ${post.title}.`, + }; + sendEmail(email_data); } } catch(err) { const { status, errors } = errorHandler(err); diff --git a/server/services/users/src/controllers/LikeController.js b/server/services/users/src/controllers/LikeController.js index 0e1408f..9c85619 100644 --- a/server/services/users/src/controllers/LikeController.js +++ b/server/services/users/src/controllers/LikeController.js @@ -1,5 +1,6 @@ const { Like, Post, User } = require('../models'); const errorHandler = require('../helpers/errorHandler'); +const sendEmail = require('../helpers/mailgun'); class LikeController { static async create(ctx) { @@ -13,13 +14,22 @@ class LikeController { where: { PostId, UserId } }); if (!like) { - const post = await Post.findByPk(PostId); + const post = await Post.findByPk(PostId, { include: [User] }); if (!post) { throw new Error('The post does not exist.'); } else { const like = await Like.create({ PostId, UserId }); ctx.response.status = 201; ctx.response.body = like; + + // Send email with Mailgun API : + const email_data = { + from: `Blog App Team `, + to: `${post.User.email}`, + subject: `Blog App - Notification`, + text: `Hello, ${post.User.username}. Someone just liked your post entitled ${post.title}.`, + }; + sendEmail(email_data); } } else { throw new Error('You cannot give more than one like for the same post.'); diff --git a/server/services/users/src/controllers/UserController.js b/server/services/users/src/controllers/UserController.js index 9eb28cf..62288bc 100644 --- a/server/services/users/src/controllers/UserController.js +++ b/server/services/users/src/controllers/UserController.js @@ -18,7 +18,7 @@ class UserController { // Send email with Mailgun API : const url = `http://localhost:3000/users/verify?token=${verification_token}`; const email_data = { - from: `Blog App `, + from: `Blog App Team `, to: `${new_user.email}`, subject: `Blog App - User Verification`, text: `Please click on this link to verify your account : ${url}`, From 4d1aea5a8497ac59409ff2bc3692ecc0bee6d257 Mon Sep 17 00:00:00 2001 From: Alfian Tirta Date: Thu, 26 Nov 2020 00:05:27 +0700 Subject: [PATCH 18/37] added send notification email when someone commented on a comment; edited notification message --- .../users/src/controllers/CommentController.js | 4 +++- .../users/src/controllers/LikeController.js | 4 +++- .../src/controllers/SubCommentController.js | 16 ++++++++++++++-- 3 files changed, 20 insertions(+), 4 deletions(-) diff --git a/server/services/users/src/controllers/CommentController.js b/server/services/users/src/controllers/CommentController.js index 8d0e1e3..ce580e2 100644 --- a/server/services/users/src/controllers/CommentController.js +++ b/server/services/users/src/controllers/CommentController.js @@ -26,7 +26,9 @@ class CommentController { from: `Blog App Team `, to: `${post.User.email}`, subject: `Blog App - Notification`, - text: `Hello, ${post.User.username}. Someone just commented on your post entitled ${post.title}.`, + text: `Hello, ${post.User.username}. + +Someone just commented "${comment.content}" on your post entitled "${post.title}".`, }; sendEmail(email_data); } diff --git a/server/services/users/src/controllers/LikeController.js b/server/services/users/src/controllers/LikeController.js index 9c85619..cbbdc40 100644 --- a/server/services/users/src/controllers/LikeController.js +++ b/server/services/users/src/controllers/LikeController.js @@ -27,7 +27,9 @@ class LikeController { from: `Blog App Team `, to: `${post.User.email}`, subject: `Blog App - Notification`, - text: `Hello, ${post.User.username}. Someone just liked your post entitled ${post.title}.`, + text: `Hello, ${post.User.username}. + +Someone just liked your post entitled "${post.title}".`, }; sendEmail(email_data); } diff --git a/server/services/users/src/controllers/SubCommentController.js b/server/services/users/src/controllers/SubCommentController.js index b857778..cf58cd0 100644 --- a/server/services/users/src/controllers/SubCommentController.js +++ b/server/services/users/src/controllers/SubCommentController.js @@ -1,5 +1,6 @@ -const { SubComment, Comment, User } = require('../models'); +const { SubComment, Comment, User, Post } = require('../models'); const errorHandler = require('../helpers/errorHandler'); +const sendEmail = require('../helpers/mailgun'); class SubCommentController { static async create(ctx) { @@ -12,13 +13,24 @@ class SubCommentController { } const { content, CommentId } = ctx.request.body; try { - const comment = await Comment.findByPk(CommentId); + const comment = await Comment.findByPk(CommentId, { include: [User, Post] }); if (!comment && content && PostId) { throw new Error('The comment does not exist.'); } else { const sub_comment = await SubComment.create({ content, CommentId, UserId }); ctx.response.status = 201; ctx.response.body = sub_comment; + + // Send email with Mailgun API : + const email_data = { + from: `Blog App Team `, + to: `${comment.User.email}`, + subject: `Blog App - Notification`, + text: `Hello, ${comment.User.username}. + +Someone just commented "${sub_comment.content}" on your comment "${comment.content}" from a post entitled "${comment.Post.title}".`, + }; + sendEmail(email_data); } } catch(err) { const { status, errors } = errorHandler(err); From cb3dd4ee2d60d18299502be7b6cc1aafc886fd16 Mon Sep 17 00:00:00 2001 From: Alfian Tirta Date: Thu, 26 Nov 2020 09:11:00 +0700 Subject: [PATCH 19/37] post search with filter feature done, include validation --- server/services/users/api_doc.md | 112 ++++++++++++++++++ .../users/src/controllers/PostController.js | 33 ++++++ .../users/src/helpers/errorHandler.js | 6 + .../services/users/src/helpers/filterPost.js | 28 +++++ .../services/users/src/routes/posts/index.js | 1 + 5 files changed, 180 insertions(+) create mode 100644 server/services/users/src/helpers/filterPost.js diff --git a/server/services/users/api_doc.md b/server/services/users/api_doc.md index a169b88..97d79c9 100644 --- a/server/services/users/api_doc.md +++ b/server/services/users/api_doc.md @@ -17,6 +17,7 @@ Its Users service has : - POST /posts - GET /posts + - GET /posts/search?title=&sort=&order= - GET /posts/:id - GET /posts/user/:id - PUT /posts/:id @@ -306,6 +307,117 @@ _Response (401 - Unauthorized)_ ``` +_Response (500 - Internal Server Error)_ +``` +[ + "Internal Server Error" +] +``` +--- +### GET /posts/search?title=&sort=&order= + +> Get blog posts with filter options +> +> This feature supports multiple sort options with hierarchical order of importance. The first sort option is the most important, and the last sort option is the least important. +> +> Available sort options : +> +> * 'date' : to sort by the date upon the creation of the post +> * 'user' : to sort by the username of the post owner +> * 'most_comments' : to sort by the total comments each post has +> * 'most_likes' : to sort by the total likes each post has +> +> Available order options : +> +> * 'asc' : ascending order to all sort options +> * 'desc' : descending order to all sort options + +_Request Header_ +``` +not needed +``` + +_Request Body_ +``` +not needed +``` + +_Response (200 - OK)_ +``` +{ + "count": , + "data": [ + { + "id": , + "title": "", + "content": "", + "UserId": , + "createdAt": "", + "updatedAt": "", + "User": { + "id": , + "username": "", + "email": "", + "password": "", + "status": "", + "createdAt": "", + "updatedAt": "" + }, + "Likes": [ + { + "id": , + "PostId": , + "UserId": , + "createdAt": "", + "updatedAt": "" + }, + ... + ], + "Comments": [ + { + "id": , + "content": "", + "PostId": , + "UserId": , + "createdAt": "", + "updatedAt": "", + "SubComments": [ + { + "id": , + "content": "", + "CommentId": , + "UserId": , + "createdAt": "", + "updatedAt": "" + }, + ... + ] + }, + ... + ] + }, + ... + ] +} +``` + +_Response (400 - Bad Request)_ +``` +[ + "", + "", + ..., + "" +] +``` + +_Response (401 - Unauthorized)_ +``` +[ + "The user is not authenticated." +] +``` + _Response (500 - Internal Server Error)_ ``` [ diff --git a/server/services/users/src/controllers/PostController.js b/server/services/users/src/controllers/PostController.js index 38472ef..ab18c96 100644 --- a/server/services/users/src/controllers/PostController.js +++ b/server/services/users/src/controllers/PostController.js @@ -1,5 +1,7 @@ const { Post, User, Like, Comment, SubComment } = require('../models'); +const { Op } = require('sequelize'); const errorHandler = require('../helpers/errorHandler'); +const filterPost = require('../helpers/filterPost'); class PostController { static async create(ctx) { @@ -91,6 +93,37 @@ class PostController { } } + static async search(ctx) { + const { title, sort, order } = ctx.request.query; + const sort_array = sort.split(','); + const sort_order = filterPost(sort_array, order); + try { + const { count, rows: data } = await Post.findAndCountAll({ + where: { + title: { + [Op.iRegexp]: `${title}`, + }, + }, + order: sort_order, + include: [{ + model: User, + }, + { + model: Like, + },{ + model: Comment, + include: [SubComment], + }], + }); + ctx.response.status = 200; + ctx.response.body = { count, data }; + } catch(err) { console.log(err); + const { status, errors } = errorHandler(err); + ctx.response.status = status; + ctx.response.body = errors; + } + } + static async update(ctx) { const id = +ctx.request.params.id; if (ctx.request.body.title === undefined) { diff --git a/server/services/users/src/helpers/errorHandler.js b/server/services/users/src/helpers/errorHandler.js index 57bb55e..9f3b61b 100644 --- a/server/services/users/src/helpers/errorHandler.js +++ b/server/services/users/src/helpers/errorHandler.js @@ -7,9 +7,15 @@ function errorHandler(err) { errors.push(error.message); }); break; + case 'SequelizeDatabaseError' : + errors.push(err.message); + break; case 'JsonWebTokenError' : errors.push(err.message); break; + case 'TypeError' : + errors.push(err.message); + break; case 'InternalServerError' : errors.push(err.message); break; diff --git a/server/services/users/src/helpers/filterPost.js b/server/services/users/src/helpers/filterPost.js new file mode 100644 index 0000000..c4030af --- /dev/null +++ b/server/services/users/src/helpers/filterPost.js @@ -0,0 +1,28 @@ +const { Sequelize } = require('sequelize'); + +function filterPost(sort_array, order) { + const sort_order = []; + sort_array.forEach(sort_option => { + switch(sort_option.toLowerCase()) { + case 'date' : + sort_order.push(['createdAt', order.toLowerCase()]); + break; + case 'user' : + sort_order.push(['User', 'username', order.toLowerCase()]); + break; + case 'most_comments' : + const most_comments = Sequelize.literal('(SELECT COUNT(*) FROM "Comments" WHERE "PostId" = "Post".id)'); + sort_order.push([most_comments, order.toLowerCase()]); + break; + case 'most_likes' : + const most_likes = Sequelize.literal('(SELECT COUNT(*) FROM "Likes" WHERE "PostId" = "Post".id)'); + sort_order.push([most_likes, order.toLowerCase()]); + break; + default : + sort_order.push(['id', order.toLowerCase()]); + } + }); + return sort_order; +} + +module.exports = filterPost; diff --git a/server/services/users/src/routes/posts/index.js b/server/services/users/src/routes/posts/index.js index 959532b..8fbf707 100644 --- a/server/services/users/src/routes/posts/index.js +++ b/server/services/users/src/routes/posts/index.js @@ -6,6 +6,7 @@ function postsRoute(router) { router .post('/posts', authentication, Controller.create) .get('/posts', Controller.read) + .get('/posts/search', Controller.search) .get('/posts/:id', Controller.findByPostId) .get('/posts/user/:id', Controller.findByUserId) .put('/posts/:id', authentication, authorization_post, Controller.update) From 793926d7ea28913aee7c2d8041acf4aab4d6e5a3 Mon Sep 17 00:00:00 2001 From: Alfian Tirta Date: Fri, 27 Nov 2020 09:55:52 +0700 Subject: [PATCH 20/37] logs service done, updated api doc --- server/services/logs/logs_api.md | 208 ++++++ server/services/logs/package-lock.json | 641 ++++++++++++++++++ server/services/logs/package.json | 21 + .../logs/src/controllers/LogController.js | 65 ++ .../services/logs/src/helpers/errorHandler.js | 21 + server/services/logs/src/index.js | 32 + server/services/logs/src/models/log.js | 29 + server/services/logs/src/routes/index.js | 7 + server/services/logs/src/routes/logs/index.js | 11 + .../src/controllers/CommentController.js | 2 +- .../users/src/controllers/LikeController.js | 2 +- .../users/src/controllers/PostController.js | 2 +- .../src/controllers/SubCommentController.js | 2 +- .../users/src/controllers/UserController.js | 2 +- server/services/users/src/index.js | 2 +- .../users/{api_doc.md => users_api.md} | 15 +- 16 files changed, 1049 insertions(+), 13 deletions(-) create mode 100644 server/services/logs/logs_api.md create mode 100644 server/services/logs/package-lock.json create mode 100644 server/services/logs/package.json create mode 100644 server/services/logs/src/controllers/LogController.js create mode 100644 server/services/logs/src/helpers/errorHandler.js create mode 100644 server/services/logs/src/index.js create mode 100644 server/services/logs/src/models/log.js create mode 100644 server/services/logs/src/routes/index.js create mode 100644 server/services/logs/src/routes/logs/index.js rename server/services/users/{api_doc.md => users_api.md} (98%) diff --git a/server/services/logs/logs_api.md b/server/services/logs/logs_api.md new file mode 100644 index 0000000..0fe915f --- /dev/null +++ b/server/services/logs/logs_api.md @@ -0,0 +1,208 @@ +# Blog App - Logs Service API +Blog App is an application to manage your blog. It has two services : Users & Logs. + +This document explains the Logs services. + +The Logs service has : + +* RESTful endpoints for CRUD operations of blog access logs. +* JSON formatted response. + +  + +## Endpoints +``` + - POST /logs + - GET /logs + - DELETE /posts/:id + - DELETE /logs +``` + +## RESTful endpoints +### POST /logs + +> Create blog access log + +_Request Header_ +``` +not needed +``` + +_Request Body_ +``` +{ + "path": "", + "user_detail": {user detail object}, + "api_access_time": , + "request_object": {api request object}, + "response_object": {api response object} +} +``` + +_Response (201 - Created)_ +``` +{ + "_id": "" + "path": "", + "user_detail": {user detail object}, + "api_access_time": , + "request_object": {api request object}, + "response_object": {api response object}, + "createdAt": "", + "updatedAt": "", + "__v": +} +``` + +_Response (400 - Bad Request)_ +``` +[ + "", + "", + ..., + "" +] +``` + +_Response (500 - Internal Server Error)_ +``` +[ + "Internal Server Error" +] +``` +--- +### GET /logs + +> Get all blog access logs + +_Request Header_ +``` +not needed +``` + +_Request Body_ +``` +not needed +``` + +_Response (200 - OK)_ +``` +[ + { + "_id": "" + "path": "", + "user_detail": {user detail object}, + "api_access_time": , + "request_object": {api request object}, + "response_object": {api response object}, + "createdAt": "", + "updatedAt": "", + "__v": + }, + ... +] +``` + +_Response (400 - Bad Request)_ +``` +[ + "", + "", + ..., + "" +] +``` + +_Response (500 - Internal Server Error)_ +``` +[ + "Internal Server Error" +] +``` +--- +### DELETE /logs/:id + +> Delete a blog access log by its id + +_Request Header_ +``` +not needed +``` + +_Request Body_ +``` +not needed +``` + +_Response (200 - OK)_ +``` +{ + "message": "Delete Log Success", + "deleted_log": { + "_id": "" + "path": "", + "user_detail": {user detail object}, + "api_access_time": , + "request_object": {api request object}, + "response_object": {api response object}, + "createdAt": "", + "updatedAt": "", + "__v": + } +} +``` + +_Response (400 - Bad Request)_ +``` +[ + "", + "", + ..., + "" +] +``` + +_Response (500 - Internal Server Error)_ +``` +[ + "Internal Server Error" +] +``` +--- +### DELETE /logs + +> Delete all (reset) blog access logs by its id + +_Request Header_ +``` +not needed +``` + +_Request Body_ +``` +not needed +``` + +_Response (200 - OK)_ +``` +{ + "message": "Reset Logs Success" +} +``` + +_Response (400 - Bad Request)_ +``` +[ + "", + "", + ..., + "" +] +``` + +_Response (500 - Internal Server Error)_ +``` +[ + "Internal Server Error" +] +``` \ No newline at end of file diff --git a/server/services/logs/package-lock.json b/server/services/logs/package-lock.json new file mode 100644 index 0000000..74bf956 --- /dev/null +++ b/server/services/logs/package-lock.json @@ -0,0 +1,641 @@ +{ + "name": "logs", + "version": "1.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "accepts": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", + "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==", + "requires": { + "mime-types": "~2.1.24", + "negotiator": "0.6.2" + } + }, + "any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha1-q8av7tzqUugJzcA3au0845Y10X8=" + }, + "bl": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/bl/-/bl-2.2.1.tgz", + "integrity": "sha512-6Pesp1w0DEX1N550i/uGV/TqucVL4AM/pgThFSN/Qq9si1/DF9aIHs1BxD8V/QU0HoeHO6cQRTAuYnLPKq1e4g==", + "requires": { + "readable-stream": "^2.3.5", + "safe-buffer": "^5.1.1" + } + }, + "bluebird": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.1.tgz", + "integrity": "sha512-MKiLiV+I1AA596t9w1sQJ8jkiSr5+ZKi0WKrYGUn6d1Fx+Ij4tIj+m2WMQSGczs5jZVxV339chE8iwk6F64wjA==" + }, + "bson": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/bson/-/bson-1.1.5.tgz", + "integrity": "sha512-kDuEzldR21lHciPQAIulLs1LZlCXdLziXI6Mb/TDkwXhb//UORJNPXgcRs2CuO4H0DcMkpfT3/ySsP3unoZjBg==" + }, + "bytes": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", + "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==" + }, + "cache-content-type": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cache-content-type/-/cache-content-type-1.0.1.tgz", + "integrity": "sha512-IKufZ1o4Ut42YUrZSo8+qnMTrFuKkvyoLXUywKz9GJ5BrhOFGhLdkx9sG4KAnVvbY6kEcSFjLQul+DVmBm2bgA==", + "requires": { + "mime-types": "^2.1.18", + "ylru": "^1.2.0" + } + }, + "co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=" + }, + "co-body": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/co-body/-/co-body-6.1.0.tgz", + "integrity": "sha512-m7pOT6CdLN7FuXUcpuz/8lfQ/L77x8SchHCF4G0RBTJO20Wzmhn5Sp4/5WsKy8OSpifBSUrmg83qEqaDHdyFuQ==", + "requires": { + "inflation": "^2.0.0", + "qs": "^6.5.2", + "raw-body": "^2.3.3", + "type-is": "^1.6.16" + } + }, + "content-disposition": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", + "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==", + "requires": { + "safe-buffer": "5.1.2" + } + }, + "content-type": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" + }, + "cookies": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/cookies/-/cookies-0.8.0.tgz", + "integrity": "sha512-8aPsApQfebXnuI+537McwYsDtjVxGm8gTIzQI3FDW6t5t/DAhERxtnbEPN/8RX+uZthoz4eCOgloXaE5cYyNow==", + "requires": { + "depd": "~2.0.0", + "keygrip": "~1.1.0" + }, + "dependencies": { + "depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" + } + } + }, + "copy-to": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/copy-to/-/copy-to-2.0.1.tgz", + "integrity": "sha1-JoD7uAaKSNCGVrYJgJK9r8kG9KU=" + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + }, + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "requires": { + "ms": "2.0.0" + } + }, + "deep-equal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz", + "integrity": "sha1-9dJgKStmDghO/0zbyfCK0yR0SLU=" + }, + "delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=" + }, + "denque": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/denque/-/denque-1.4.1.tgz", + "integrity": "sha512-OfzPuSZKGcgr96rf1oODnfjqBFmr1DVoc/TrItj3Ohe0Ah1C5WX5Baquw/9U9KovnQ88EqmJbD66rKYUQYN1tQ==" + }, + "depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" + }, + "destroy": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", + "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" + }, + "dotenv": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.2.0.tgz", + "integrity": "sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw==" + }, + "ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" + }, + "encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" + }, + "escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" + }, + "fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" + }, + "http-assert": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/http-assert/-/http-assert-1.4.1.tgz", + "integrity": "sha512-rdw7q6GTlibqVVbXr0CKelfV5iY8G2HqEUkhSk297BMbSpSL8crXC+9rjKoMcZZEsksX30le6f/4ul4E28gegw==", + "requires": { + "deep-equal": "~1.0.1", + "http-errors": "~1.7.2" + }, + "dependencies": { + "http-errors": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.3.tgz", + "integrity": "sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw==", + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.4", + "setprototypeof": "1.1.1", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" + } + } + } + }, + "http-errors": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.0.tgz", + "integrity": "sha512-4I8r0C5JDhT5VkvI47QktDW75rNlGVsUf/8hzjCC/wkWI/jdTRmBb9aI7erSG82r1bjKY3F6k28WnsVxB1C73A==", + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" + }, + "dependencies": { + "setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + } + } + }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "inflation": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/inflation/-/inflation-2.0.0.tgz", + "integrity": "sha1-i0F+R8KPklpFEz2RTKH9OJEH8w8=" + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "is-generator-function": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.7.tgz", + "integrity": "sha512-YZc5EwyO4f2kWCax7oegfuSr9mFz1ZvieNYBEjmukLxgXfBUbxAWGVF7GZf0zidYtoBl3WvC07YK0wT76a+Rtw==" + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "kareem": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.3.1.tgz", + "integrity": "sha512-l3hLhffs9zqoDe8zjmb/mAN4B8VT3L56EUvKNqLFVs9YlFA+zx7ke1DO8STAdDyYNkeSo1nKmjuvQeI12So8Xw==" + }, + "kcors": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/kcors/-/kcors-2.2.2.tgz", + "integrity": "sha512-rIqbKa2S0gT0wC/790jsQM6hNpABHBNWQ7+XYS1xJV6zOGxlanW+RtCmlDn6wPZsGpRk371yy8abfBgl2OTavg==" + }, + "keygrip": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/keygrip/-/keygrip-1.1.0.tgz", + "integrity": "sha512-iYSchDJ+liQ8iwbSI2QqsQOvqv58eJCEanyJPJi+Khyu8smkcKSFUCbPwzFcL7YVtZ6eONjqRX/38caJ7QjRAQ==", + "requires": { + "tsscmp": "1.0.6" + } + }, + "koa": { + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/koa/-/koa-2.13.0.tgz", + "integrity": "sha512-i/XJVOfPw7npbMv67+bOeXr3gPqOAw6uh5wFyNs3QvJ47tUx3M3V9rIE0//WytY42MKz4l/MXKyGkQ2LQTfLUQ==", + "requires": { + "accepts": "^1.3.5", + "cache-content-type": "^1.0.0", + "content-disposition": "~0.5.2", + "content-type": "^1.0.4", + "cookies": "~0.8.0", + "debug": "~3.1.0", + "delegates": "^1.0.0", + "depd": "^1.1.2", + "destroy": "^1.0.4", + "encodeurl": "^1.0.2", + "escape-html": "^1.0.3", + "fresh": "~0.5.2", + "http-assert": "^1.3.0", + "http-errors": "^1.6.3", + "is-generator-function": "^1.0.7", + "koa-compose": "^4.1.0", + "koa-convert": "^1.2.0", + "on-finished": "^2.3.0", + "only": "~0.0.2", + "parseurl": "^1.3.2", + "statuses": "^1.5.0", + "type-is": "^1.6.16", + "vary": "^1.1.2" + } + }, + "koa-bodyparser": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/koa-bodyparser/-/koa-bodyparser-4.3.0.tgz", + "integrity": "sha512-uyV8G29KAGwZc4q/0WUAjH+Tsmuv9ImfBUF2oZVyZtaeo0husInagyn/JH85xMSxM0hEk/mbCII5ubLDuqW/Rw==", + "requires": { + "co-body": "^6.0.0", + "copy-to": "^2.0.1" + } + }, + "koa-compose": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/koa-compose/-/koa-compose-4.1.0.tgz", + "integrity": "sha512-8ODW8TrDuMYvXRwra/Kh7/rJo9BtOfPc6qO8eAfC80CnCvSjSl0bkRM24X6/XBBEyj0v1nRUQ1LyOy3dbqOWXw==" + }, + "koa-convert": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/koa-convert/-/koa-convert-1.2.0.tgz", + "integrity": "sha1-2kCHXfSd4FOQmNFwC1CCDOvNIdA=", + "requires": { + "co": "^4.6.0", + "koa-compose": "^3.0.0" + }, + "dependencies": { + "koa-compose": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/koa-compose/-/koa-compose-3.2.1.tgz", + "integrity": "sha1-qFzLQLfZhtjlo0Wzoazo6rz1Tec=", + "requires": { + "any-promise": "^1.1.0" + } + } + } + }, + "koa-router": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/koa-router/-/koa-router-10.0.0.tgz", + "integrity": "sha512-gAE5J1gBQTvfR8rMMtMUkE26+1MbO3DGpGmvfmM2pR9Z7w2VIb2Ecqeal98yVO7+4ltffby7gWOzpCmdNOQe0w==", + "requires": { + "debug": "^4.1.1", + "http-errors": "^1.7.3", + "koa-compose": "^4.1.0", + "methods": "^1.1.2", + "path-to-regexp": "^6.1.0" + }, + "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==" + } + } + }, + "media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" + }, + "memory-pager": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", + "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==", + "optional": true + }, + "methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" + }, + "mime-db": { + "version": "1.44.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.44.0.tgz", + "integrity": "sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg==" + }, + "mime-types": { + "version": "2.1.27", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.27.tgz", + "integrity": "sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w==", + "requires": { + "mime-db": "1.44.0" + } + }, + "mongodb": { + "version": "3.6.3", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-3.6.3.tgz", + "integrity": "sha512-rOZuR0QkodZiM+UbQE5kDsJykBqWi0CL4Ec2i1nrGrUI3KO11r6Fbxskqmq3JK2NH7aW4dcccBuUujAP0ERl5w==", + "requires": { + "bl": "^2.2.1", + "bson": "^1.1.4", + "denque": "^1.4.1", + "require_optional": "^1.0.1", + "safe-buffer": "^5.1.2", + "saslprep": "^1.0.0" + } + }, + "mongoose": { + "version": "5.10.16", + "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-5.10.16.tgz", + "integrity": "sha512-rgfK1lvAQdCZ0buPju7Wny3suls5A1GjYRsv+jrQBVA0N/OhtGKHjr5RXJs0rxQhodwNVfc7O8g4bwDqW4R0sQ==", + "requires": { + "bson": "^1.1.4", + "kareem": "2.3.1", + "mongodb": "3.6.3", + "mongoose-legacy-pluralize": "1.0.2", + "mpath": "0.7.0", + "mquery": "3.2.2", + "ms": "2.1.2", + "regexp-clone": "1.0.0", + "safe-buffer": "5.2.1", + "sift": "7.0.1", + "sliced": "1.0.1" + }, + "dependencies": { + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + } + } + }, + "mongoose-legacy-pluralize": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/mongoose-legacy-pluralize/-/mongoose-legacy-pluralize-1.0.2.tgz", + "integrity": "sha512-Yo/7qQU4/EyIS8YDFSeenIvXxZN+ld7YdV9LqFVQJzTLye8unujAWPZ4NWKfFA+RNjh+wvTWKY9Z3E5XM6ZZiQ==" + }, + "mpath": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.7.0.tgz", + "integrity": "sha512-Aiq04hILxhz1L+f7sjGyn7IxYzWm1zLNNXcfhDtx04kZ2Gk7uvFdgZ8ts1cWa/6d0TQmag2yR8zSGZUmp0tFNg==" + }, + "mquery": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/mquery/-/mquery-3.2.2.tgz", + "integrity": "sha512-XB52992COp0KP230I3qloVUbkLUxJIu328HBP2t2EsxSFtf4W1HPSOBWOXf1bqxK4Xbb66lfMJ+Bpfd9/yZE1Q==", + "requires": { + "bluebird": "3.5.1", + "debug": "3.1.0", + "regexp-clone": "^1.0.0", + "safe-buffer": "5.1.2", + "sliced": "1.0.1" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "negotiator": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", + "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==" + }, + "on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", + "requires": { + "ee-first": "1.1.1" + } + }, + "only": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/only/-/only-0.0.2.tgz", + "integrity": "sha1-Kv3oTQPlC5qO3EROMGEKcCle37Q=" + }, + "parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" + }, + "path-to-regexp": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.2.0.tgz", + "integrity": "sha512-f66KywYG6+43afgE/8j/GoiNyygk/bnoCbps++3ErRKsIYkGGupyv07R2Ok5m9i67Iqc+T2g1eAUGUPzWhYTyg==" + }, + "process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + }, + "qs": { + "version": "6.9.4", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.9.4.tgz", + "integrity": "sha512-A1kFqHekCTM7cz0udomYUoYNWjBebHm/5wzU/XqrBRBNWectVH0QIiN+NEcZ0Dte5hvzHwbr8+XQmguPhJ6WdQ==" + }, + "raw-body": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.1.tgz", + "integrity": "sha512-9WmIKF6mkvA0SLmA2Knm9+qj89e+j1zqgyn8aXGd7+nAduPoqgI9lO57SAZNn/Byzo5P7JhXTyg9PzaJbH73bA==", + "requires": { + "bytes": "3.1.0", + "http-errors": "1.7.3", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "dependencies": { + "http-errors": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.3.tgz", + "integrity": "sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw==", + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.4", + "setprototypeof": "1.1.1", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" + } + } + } + }, + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "regexp-clone": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/regexp-clone/-/regexp-clone-1.0.0.tgz", + "integrity": "sha512-TuAasHQNamyyJ2hb97IuBEif4qBHGjPHBS64sZwytpLEqtBQ1gPJTnOaQ6qmpET16cK14kkjbazl6+p0RRv0yw==" + }, + "require_optional": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/require_optional/-/require_optional-1.0.1.tgz", + "integrity": "sha512-qhM/y57enGWHAe3v/NcwML6a3/vfESLe/sGM2dII+gEO0BpKRUkWZow/tyloNqJyN6kXSl3RyyM8Ll5D/sJP8g==", + "requires": { + "resolve-from": "^2.0.0", + "semver": "^5.1.0" + } + }, + "resolve-from": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-2.0.0.tgz", + "integrity": "sha1-lICrIOlP+h2egKgEx+oUdhGWa1c=" + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "saslprep": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/saslprep/-/saslprep-1.0.3.tgz", + "integrity": "sha512-/MY/PEMbk2SuY5sScONwhUDsV2p77Znkb/q3nSVstq/yQzYJOH/Azh29p9oJLsl3LnQwSvZDKagDGBsBwSooag==", + "optional": true, + "requires": { + "sparse-bitfield": "^3.0.3" + } + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" + }, + "setprototypeof": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", + "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" + }, + "sift": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/sift/-/sift-7.0.1.tgz", + "integrity": "sha512-oqD7PMJ+uO6jV9EQCl0LrRw1OwsiPsiFQR5AR30heR+4Dl7jBBbDLnNvWiak20tzZlSE1H7RB30SX/1j/YYT7g==" + }, + "sliced": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/sliced/-/sliced-1.0.1.tgz", + "integrity": "sha1-CzpmK10Ewxd7GSa+qCsD+Dei70E=" + }, + "sparse-bitfield": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", + "integrity": "sha1-/0rm5oZWBWuks+eSqzM004JzyhE=", + "optional": true, + "requires": { + "memory-pager": "^1.0.2" + } + }, + "statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "toidentifier": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", + "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==" + }, + "tsscmp": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.6.tgz", + "integrity": "sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==" + }, + "type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "requires": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + } + }, + "unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + }, + "vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" + }, + "ylru": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ylru/-/ylru-1.2.1.tgz", + "integrity": "sha512-faQrqNMzcPCHGVC2aaOINk13K+aaBDUPjGWl0teOXywElLjyVAB6Oe2jj62jHYtwsU49jXhScYbvPENK+6zAvQ==" + } + } +} diff --git a/server/services/logs/package.json b/server/services/logs/package.json new file mode 100644 index 0000000..2e25bd1 --- /dev/null +++ b/server/services/logs/package.json @@ -0,0 +1,21 @@ +{ + "name": "logs", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "start": "nodemon src", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "dotenv": "^8.2.0", + "kcors": "^2.2.2", + "koa": "^2.13.0", + "koa-bodyparser": "^4.3.0", + "koa-router": "^10.0.0", + "mongoose": "^5.10.16" + } +} diff --git a/server/services/logs/src/controllers/LogController.js b/server/services/logs/src/controllers/LogController.js new file mode 100644 index 0000000..963713d --- /dev/null +++ b/server/services/logs/src/controllers/LogController.js @@ -0,0 +1,65 @@ +const Log = require('../models/log'); +const errorHandler = require('../helpers/errorHandler'); + +class LogController { + static async create(ctx) { + try { + const new_log = new Log(ctx.request.body); + await new_log.save(); + ctx.response.status = 201; + ctx.response.body = new_log; + } catch(err) { + const { status, errors } = errorHandler(err); + ctx.response.status = status; + ctx.response.body = errors; + } + } + + static async read(ctx) { + try { + const all_logs = await Log.find({}); + ctx.response.status = 200; + ctx.response.body = all_logs; + } catch(err) { + const { status, errors } = errorHandler(err); + ctx.response.status = status; + ctx.response.body = errors; + } + } + + static async delete(ctx) { + const id = ctx.request.params.id; + try { + const deleted_log = await Log.findByIdAndDelete(id); + if (!deleted_log) { + throw new Error('The log does not exist.'); + } else { + ctx.response.status = 200; + ctx.response.body = { + message: 'Delete Log Success', + deleted_log, + }; + } + } catch(err) { + const { status, errors } = errorHandler(err); + ctx.response.status = status; + ctx.response.body = errors; + } + } + + static async reset(ctx) { + try { + const reset_result = await Log.deleteMany({}); + ctx.response.status = 200; + ctx.response.body = { + message: 'Reset Logs Success', + }; + } catch(err) { + const { status, errors } = errorHandler(err); + ctx.response.status = status; + ctx.response.body = errors; + } + } +} + +module.exports = LogController; diff --git a/server/services/logs/src/helpers/errorHandler.js b/server/services/logs/src/helpers/errorHandler.js new file mode 100644 index 0000000..8eee50b --- /dev/null +++ b/server/services/logs/src/helpers/errorHandler.js @@ -0,0 +1,21 @@ +function errorHandler(err) { + const errors = []; + let status = 400; + switch(err.name) { + case 'ValidationError' : + err.message.split(', ').forEach(msg => { + const temp = msg.split(': '); + errors.push(temp[temp.length - 1]); + }); + break; + case 'Error' : + errors.push(err.message); + break; + default : + errors.push('Internal Server Error'); + status = 500; + } + return { status, errors }; +} + +module.exports = errorHandler; diff --git a/server/services/logs/src/index.js b/server/services/logs/src/index.js new file mode 100644 index 0000000..5046c63 --- /dev/null +++ b/server/services/logs/src/index.js @@ -0,0 +1,32 @@ +'use strict'; + +require('dotenv').config(); +const cors = require('kcors'); +const mongoose = require('mongoose'); +const Koa = require('koa'); +const Router = require('koa-router'); +const bodyParser = require('koa-bodyparser'); +const loadRoutes = require('./routes'); +const app = new Koa(); +const router = new Router(); +const port = process.env.PORT || 3002; + +mongoose.connect(process.env.DB_URI, { + useNewUrlParser: true, + useUnifiedTopology: true, +}) + .then(result => { + console.log('successfully connected to the database'); + app.listen(port, () => { + console.log(`logs service running at http://localhost:${port}`); + }); + }) + .catch(err => console.log(err)); + +app + .use(cors()) + .use(bodyParser()) + .use(router.routes()) + .use(router.allowedMethods()); + +loadRoutes(router); diff --git a/server/services/logs/src/models/log.js b/server/services/logs/src/models/log.js new file mode 100644 index 0000000..8dc7089 --- /dev/null +++ b/server/services/logs/src/models/log.js @@ -0,0 +1,29 @@ +const mongoose = require('mongoose'); + +const LogSchema = new mongoose.Schema({ + path: { + type: String, + required: true, + trim: true, + }, + user_detail: { + type: Object, + required: true, + }, + api_access_time: { + type: Number, + required: true, + }, + request_object: { + type: Object, + required: true, + }, + response_object: { + type: Object, + required: true, + }, +}, { timestamps: true }); + +const Log = mongoose.model('Log', LogSchema); + +module.exports = Log; diff --git a/server/services/logs/src/routes/index.js b/server/services/logs/src/routes/index.js new file mode 100644 index 0000000..147cee9 --- /dev/null +++ b/server/services/logs/src/routes/index.js @@ -0,0 +1,7 @@ +const logsRoute = require('./logs'); + +function loadRoutes(router) { + logsRoute(router); +} + +module.exports = loadRoutes; diff --git a/server/services/logs/src/routes/logs/index.js b/server/services/logs/src/routes/logs/index.js new file mode 100644 index 0000000..36514a2 --- /dev/null +++ b/server/services/logs/src/routes/logs/index.js @@ -0,0 +1,11 @@ +const Controller = require('../../controllers/LogController'); + +function logsRoute(router) { + router + .post('/logs', Controller.create) + .get('/logs', Controller.read) + .delete('/logs/:id', Controller.delete) + .delete('/logs', Controller.reset); +}; + +module.exports = logsRoute; diff --git a/server/services/users/src/controllers/CommentController.js b/server/services/users/src/controllers/CommentController.js index ce580e2..136f691 100644 --- a/server/services/users/src/controllers/CommentController.js +++ b/server/services/users/src/controllers/CommentController.js @@ -46,7 +46,7 @@ Someone just commented "${comment.content}" on your post entitled "${post.title} await Comment.destroy({ where: { id }}); ctx.response.status = 200; ctx.response.body = { - message: 'Delete Success', + message: 'Delete Comment Success', deleted_comment, }; } catch(err) { diff --git a/server/services/users/src/controllers/LikeController.js b/server/services/users/src/controllers/LikeController.js index cbbdc40..78107dc 100644 --- a/server/services/users/src/controllers/LikeController.js +++ b/server/services/users/src/controllers/LikeController.js @@ -50,7 +50,7 @@ Someone just liked your post entitled "${post.title}".`, await Like.destroy({ where: { id } }); ctx.response.status = 200; ctx.response.body = { - message: 'Delete Success', + message: 'Delete Like Success', deleted_like, }; } catch(err) { diff --git a/server/services/users/src/controllers/PostController.js b/server/services/users/src/controllers/PostController.js index ab18c96..e03af2d 100644 --- a/server/services/users/src/controllers/PostController.js +++ b/server/services/users/src/controllers/PostController.js @@ -154,7 +154,7 @@ class PostController { await Post.destroy({ where: { id } }); ctx.response.status = 200; ctx.response.body = { - message: 'Delete Success', + message: 'Delete Post Success', deleted_post, }; } catch(err) { diff --git a/server/services/users/src/controllers/SubCommentController.js b/server/services/users/src/controllers/SubCommentController.js index cf58cd0..d94533e 100644 --- a/server/services/users/src/controllers/SubCommentController.js +++ b/server/services/users/src/controllers/SubCommentController.js @@ -46,7 +46,7 @@ Someone just commented "${sub_comment.content}" on your comment "${comment.conte await SubComment.destroy({ where: { id }}); ctx.response.status = 200; ctx.response.body = { - message: 'Delete Success', + message: 'Delete Sub Comment Success', deleted_sub_comment, }; } catch(err) { diff --git a/server/services/users/src/controllers/UserController.js b/server/services/users/src/controllers/UserController.js index 62288bc..9c28d6e 100644 --- a/server/services/users/src/controllers/UserController.js +++ b/server/services/users/src/controllers/UserController.js @@ -16,7 +16,7 @@ class UserController { const verification_token = generate_jwt_token(new_user); // Send email with Mailgun API : - const url = `http://localhost:3000/users/verify?token=${verification_token}`; + const url = `http://localhost:3001/users/verify?token=${verification_token}`; const email_data = { from: `Blog App Team `, to: `${new_user.email}`, diff --git a/server/services/users/src/index.js b/server/services/users/src/index.js index ba167a9..674ea36 100644 --- a/server/services/users/src/index.js +++ b/server/services/users/src/index.js @@ -8,7 +8,7 @@ const bodyParser = require('koa-bodyparser'); const loadRoutes = require('./routes'); const app = new Koa(); const router = new Router(); -const port = process.env.PORT || 3000; +const port = process.env.PORT || 3001; app .use(cors()) diff --git a/server/services/users/api_doc.md b/server/services/users/users_api.md similarity index 98% rename from server/services/users/api_doc.md rename to server/services/users/users_api.md index 97d79c9..13ff8ed 100644 --- a/server/services/users/api_doc.md +++ b/server/services/users/users_api.md @@ -1,7 +1,9 @@ # Blog App - Users Service API Blog App is an application to manage your blog. It has two services : Users & Logs. -Its Users service has : +This document explains the Users services. + +The Users service has : * RESTful endpoints for user registration, verification, and login. * RESTful endpoints for CRUD operations of blog posts, likes, comments, and sub comments. @@ -186,7 +188,7 @@ _Request Body_ _Response (201 - Created)_ ``` { - "id": 1, + "id": , "title": "", "content": "", "UserId": , @@ -306,7 +308,6 @@ _Response (401 - Unauthorized)_ ] ``` - _Response (500 - Internal Server Error)_ ``` [ @@ -691,7 +692,7 @@ not needed _Response (200 - OK)_ ``` { - "message": "Delete Success", + "message": "Delete Post Success", "deleted_post": { "id": , "title": "", @@ -815,7 +816,7 @@ not needed _Response (200 - OK)_ ``` { - "message": "Delete Success", + "message": "Delete Comment Success", "deleted_comment": { "id": , "content": "", @@ -939,7 +940,7 @@ not needed _Response (200 - OK)_ ``` { - "message": "Delete Success", + "message": "Delete Sub Comment Success", "deleted_sub_comment": { "id": , "content": "", @@ -1061,7 +1062,7 @@ not needed _Response (200 - OK)_ ``` { - "message": "Delete Success", + "message": "Delete Like Success", "deleted_like": { "id": , "PostId": "", From d4afc4c2cd52c4490411a5a487c7fd4f1227dd71 Mon Sep 17 00:00:00 2001 From: Alfian Tirta Date: Fri, 27 Nov 2020 10:09:40 +0700 Subject: [PATCH 21/37] updated blog_app_erd.png --- blog_app_erd.png | Bin 47550 -> 49176 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/blog_app_erd.png b/blog_app_erd.png index b982d8a9f36527802e8c5ffeacb0ddc2da70a179..52b57bd0339c55e5ff255b5bfa7ed128a285621b 100644 GIT binary patch delta 29738 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 BL_Poj delta 28099 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 Ju#Fr-{tk-CGT#6I From 166754a87dd16a3572a2f30bcbd49ee6bf12ddc9 Mon Sep 17 00:00:00 2001 From: Alfian Tirta Date: Mon, 30 Nov 2020 13:13:09 +0700 Subject: [PATCH 22/37] development done, deploy on progress --- .../services/logs/logs_api.md => logs_api.md | 28 +- .../graphqlOrchestrator/package-lock.json | 1512 +++++++++++++++++ .../graphqlOrchestrator/package.json | 19 + .../src/dataSources/logsAPI.js | 23 + .../src/dataSources/usersAPI.js | 104 ++ .../graphqlOrchestrator/src/index.js | 23 + .../src/resolvers/index.js | 54 + .../src/resolvers/mutations/index.js | 77 + .../src/resolvers/queries/index.js | 43 + .../graphqlOrchestrator/src/schema/index.js | 12 + .../src/schema/models/index.js | 128 ++ .../src/schema/mutations/index.js | 86 + .../src/schema/queries/index.js | 23 + server/services/users/package-lock.json | 13 + server/services/users/package.json | 1 + .../src/controllers/CommentController.js | 31 + .../users/src/controllers/LikeController.js | 31 + .../users/src/controllers/PostController.js | 108 +- .../src/controllers/SubCommentController.js | 31 + .../users/src/controllers/UserController.js | 69 +- server/services/users/src/helpers/logger.js | 32 + .../users/src/middlewares/authentication.js | 9 + .../users/src/middlewares/authorization.js | 35 +- .../users/users_api.md => users_api.md | 119 +- 24 files changed, 2520 insertions(+), 91 deletions(-) rename server/services/logs/logs_api.md => logs_api.md (87%) create mode 100644 server/orchestrators/graphqlOrchestrator/package-lock.json create mode 100644 server/orchestrators/graphqlOrchestrator/package.json create mode 100644 server/orchestrators/graphqlOrchestrator/src/dataSources/logsAPI.js create mode 100644 server/orchestrators/graphqlOrchestrator/src/dataSources/usersAPI.js create mode 100644 server/orchestrators/graphqlOrchestrator/src/index.js create mode 100644 server/orchestrators/graphqlOrchestrator/src/resolvers/index.js create mode 100644 server/orchestrators/graphqlOrchestrator/src/resolvers/mutations/index.js create mode 100644 server/orchestrators/graphqlOrchestrator/src/resolvers/queries/index.js create mode 100644 server/orchestrators/graphqlOrchestrator/src/schema/index.js create mode 100644 server/orchestrators/graphqlOrchestrator/src/schema/models/index.js create mode 100644 server/orchestrators/graphqlOrchestrator/src/schema/mutations/index.js create mode 100644 server/orchestrators/graphqlOrchestrator/src/schema/queries/index.js create mode 100644 server/services/users/src/helpers/logger.js rename server/services/users/users_api.md => users_api.md (91%) diff --git a/server/services/logs/logs_api.md b/logs_api.md similarity index 87% rename from server/services/logs/logs_api.md rename to logs_api.md index 0fe915f..4120387 100644 --- a/server/services/logs/logs_api.md +++ b/logs_api.md @@ -5,7 +5,7 @@ This document explains the Logs services. The Logs service has : -* RESTful endpoints for CRUD operations of blog access logs. +* RESTful endpoints for create, read, delete, and reset operations of blog access logs. * JSON formatted response.   @@ -33,7 +33,7 @@ _Request Body_ { "path": "", "user_detail": {user detail object}, - "api_access_time": , + "api_access_time": , "request_object": {api request object}, "response_object": {api response object} } @@ -57,10 +57,8 @@ _Response (201 - Created)_ _Response (400 - Bad Request)_ ``` [ - "", - "", - ..., - "" + "", + ... ] ``` @@ -106,10 +104,8 @@ _Response (200 - OK)_ _Response (400 - Bad Request)_ ``` [ - "", - "", - ..., - "" + "", + ... ] ``` @@ -155,10 +151,8 @@ _Response (200 - OK)_ _Response (400 - Bad Request)_ ``` [ - "", - "", - ..., - "" + "", + ... ] ``` @@ -193,10 +187,8 @@ _Response (200 - OK)_ _Response (400 - Bad Request)_ ``` [ - "", - "", - ..., - "" + "", + ... ] ``` diff --git a/server/orchestrators/graphqlOrchestrator/package-lock.json b/server/orchestrators/graphqlOrchestrator/package-lock.json new file mode 100644 index 0000000..037c035 --- /dev/null +++ b/server/orchestrators/graphqlOrchestrator/package-lock.json @@ -0,0 +1,1512 @@ +{ + "name": "graphqlOrchestrator", + "version": "1.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@apollo/protobufjs": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@apollo/protobufjs/-/protobufjs-1.0.5.tgz", + "integrity": "sha512-ZtyaBH1icCgqwIGb3zrtopV2D5Q8yxibkJzlaViM08eOhTQc7rACdYu0pfORFfhllvdMZ3aq69vifYHszY4gNA==", + "requires": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/long": "^4.0.0", + "@types/node": "^10.1.0", + "long": "^4.0.0" + }, + "dependencies": { + "@types/node": { + "version": "10.17.47", + "resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.47.tgz", + "integrity": "sha512-YZ1mMAdUPouBZCdeugjV8y1tqqr28OyL8DYbH5ePCfe9zcXtvbh1wDBy7uzlHkXo3Qi07kpzXfvycvrkby/jXw==" + } + } + }, + "@apollographql/apollo-tools": { + "version": "0.4.8", + "resolved": "https://registry.npmjs.org/@apollographql/apollo-tools/-/apollo-tools-0.4.8.tgz", + "integrity": "sha512-W2+HB8Y7ifowcf3YyPHgDI05izyRtOeZ4MqIr7LbTArtmJ0ZHULWpn84SGMW7NAvTV1tFExpHlveHhnXuJfuGA==", + "requires": { + "apollo-env": "^0.6.5" + } + }, + "@apollographql/graphql-playground-html": { + "version": "1.6.26", + "resolved": "https://registry.npmjs.org/@apollographql/graphql-playground-html/-/graphql-playground-html-1.6.26.tgz", + "integrity": "sha512-XAwXOIab51QyhBxnxySdK3nuMEUohhDsHQ5Rbco/V1vjlP75zZ0ZLHD9dTpXTN8uxKxopb2lUvJTq+M4g2Q0HQ==", + "requires": { + "xss": "^1.0.6" + } + }, + "@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha1-m4sMxmPWaafY9vXQiToU00jzD78=" + }, + "@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==" + }, + "@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==" + }, + "@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha1-NVy8mLr61ZePntCV85diHx0Ga3A=" + }, + "@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha1-upn7WYYUr2VwDBYZ/wbUVLDYTEU=", + "requires": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha1-Xp4avctz/Ap8uLKR33jIy9l7h9E=" + }, + "@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha1-/yAOPnzyQp4tyvwRQIKOjMY48Ik=" + }, + "@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha1-bMKyDFya1q0NzP0hynZz2Nf79o0=" + }, + "@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha1-Cf0V8tbTq/qbZbw2ZQbWrXhG/1Q=" + }, + "@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA=" + }, + "@types/accepts": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/accepts/-/accepts-1.3.5.tgz", + "integrity": "sha512-jOdnI/3qTpHABjM5cx1Hc0sKsPoYCp+DP/GJRGtDlPd7fiV9oXGGIcjW/ZOxLIvjGz8MA+uMZI9metHlgqbgwQ==", + "requires": { + "@types/node": "*" + } + }, + "@types/body-parser": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.0.tgz", + "integrity": "sha512-W98JrE0j2K78swW4ukqMleo8R7h/pFETjM2DQ90MF6XK2i4LO4W3gQ71Lt4w3bfm2EvVSyWHplECvB5sK22yFQ==", + "requires": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "@types/connect": { + "version": "3.4.33", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.33.tgz", + "integrity": "sha512-2+FrkXY4zllzTNfJth7jOqEHC+enpLeGslEhpnTAkg21GkRrWV4SsAtqchtT4YS9/nODBU2/ZfsBY2X4J/dX7A==", + "requires": { + "@types/node": "*" + } + }, + "@types/content-disposition": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@types/content-disposition/-/content-disposition-0.5.3.tgz", + "integrity": "sha512-P1bffQfhD3O4LW0ioENXUhZ9OIa0Zn+P7M+pWgkCKaT53wVLSq0mrKksCID/FGHpFhRSxRGhgrQmfhRuzwtKdg==" + }, + "@types/cookies": { + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/@types/cookies/-/cookies-0.7.5.tgz", + "integrity": "sha512-3+TAFSm78O7/bAeYdB8FoYGntuT87vVP9JKuQRL8sRhv9313LP2SpHHL50VeFtnyjIcb3UELddMk5Yt0eOSOkg==", + "requires": { + "@types/connect": "*", + "@types/express": "*", + "@types/keygrip": "*", + "@types/node": "*" + } + }, + "@types/cors": { + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.8.tgz", + "integrity": "sha512-fO3gf3DxU2Trcbr75O7obVndW/X5k8rJNZkLXlQWStTHhP71PkRqjwPIEI0yMnJdg9R9OasjU+Bsr+Hr1xy/0w==", + "requires": { + "@types/express": "*" + } + }, + "@types/express": { + "version": "4.17.9", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.9.tgz", + "integrity": "sha512-SDzEIZInC4sivGIFY4Sz1GG6J9UObPwCInYJjko2jzOf/Imx/dlpume6Xxwj1ORL82tBbmN4cPDIDkLbWHk9hw==", + "requires": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "*", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "@types/express-serve-static-core": { + "version": "4.17.14", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.14.tgz", + "integrity": "sha512-uFTLwu94TfUFMToXNgRZikwPuZdOtDgs3syBtAIr/OXorL1kJqUJT9qCLnRZ5KBOWfZQikQ2xKgR2tnDj1OgDA==", + "requires": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*" + } + }, + "@types/fs-capacitor": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/fs-capacitor/-/fs-capacitor-2.0.0.tgz", + "integrity": "sha512-FKVPOCFbhCvZxpVAMhdBdTfVfXUpsh15wFHgqOKxh9N9vzWZVuWCSijZ5T4U34XYNnuj2oduh6xcs1i+LPI+BQ==", + "requires": { + "@types/node": "*" + } + }, + "@types/graphql-upload": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/@types/graphql-upload/-/graphql-upload-8.0.4.tgz", + "integrity": "sha512-0TRyJD2o8vbkmJF8InppFcPVcXKk+Rvlg/xvpHBIndSJYpmDWfmtx/ZAtl4f3jR2vfarpTqYgj8MZuJssSoU7Q==", + "requires": { + "@types/express": "*", + "@types/fs-capacitor": "*", + "@types/koa": "*", + "graphql": "^15.3.0" + } + }, + "@types/http-assert": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@types/http-assert/-/http-assert-1.5.1.tgz", + "integrity": "sha512-PGAK759pxyfXE78NbKxyfRcWYA/KwW17X290cNev/qAsn9eQIxkH4shoNBafH37wewhDG/0p1cHPbK6+SzZjWQ==" + }, + "@types/http-errors": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-1.8.0.tgz", + "integrity": "sha512-2aoSC4UUbHDj2uCsCxcG/vRMXey/m17bC7UwitVm5hn22nI8O8Y9iDpA76Orc+DWkQ4zZrOKEshCqR/jSuXAHA==" + }, + "@types/keygrip": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@types/keygrip/-/keygrip-1.0.2.tgz", + "integrity": "sha512-GJhpTepz2udxGexqos8wgaBx4I/zWIDPh/KOGEwAqtuGDkOUJu5eFvwmdBX4AmB8Odsr+9pHCQqiAqDL/yKMKw==" + }, + "@types/koa": { + "version": "2.11.6", + "resolved": "https://registry.npmjs.org/@types/koa/-/koa-2.11.6.tgz", + "integrity": "sha512-BhyrMj06eQkk04C97fovEDQMpLpd2IxCB4ecitaXwOKGq78Wi2tooaDOWOFGajPk8IkQOAtMppApgSVkYe1F/A==", + "requires": { + "@types/accepts": "*", + "@types/content-disposition": "*", + "@types/cookies": "*", + "@types/http-assert": "*", + "@types/http-errors": "*", + "@types/keygrip": "*", + "@types/koa-compose": "*", + "@types/node": "*" + } + }, + "@types/koa-compose": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/@types/koa-compose/-/koa-compose-3.2.5.tgz", + "integrity": "sha512-B8nG/OoE1ORZqCkBVsup/AKcvjdgoHnfi4pZMn5UwAPCbhk/96xyv284eBYW8JlQbQ7zDmnpFr68I/40mFoIBQ==", + "requires": { + "@types/koa": "*" + } + }, + "@types/long": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.1.tgz", + "integrity": "sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w==" + }, + "@types/mime": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-2.0.3.tgz", + "integrity": "sha512-Jus9s4CDbqwocc5pOAnh8ShfrnMcPHuJYzVcSUU7lrh8Ni5HuIqX3oilL86p3dlTrk0LzHRCgA/GQ7uNCw6l2Q==" + }, + "@types/node": { + "version": "14.14.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.10.tgz", + "integrity": "sha512-J32dgx2hw8vXrSbu4ZlVhn1Nm3GbeCFNw2FWL8S5QKucHGY0cyNwjdQdO+KMBZ4wpmC7KhLCiNsdk1RFRIYUQQ==" + }, + "@types/node-fetch": { + "version": "2.5.7", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.5.7.tgz", + "integrity": "sha512-o2WVNf5UhWRkxlf6eq+jMZDu7kjgpgJfl4xVNlvryc95O/6F2ld8ztKX+qu+Rjyet93WAWm5LjeX9H5FGkODvw==", + "requires": { + "@types/node": "*", + "form-data": "^3.0.0" + } + }, + "@types/qs": { + "version": "6.9.5", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.5.tgz", + "integrity": "sha512-/JHkVHtx/REVG0VVToGRGH2+23hsYLHdyG+GrvoUGlGAd0ErauXDyvHtRI/7H7mzLm+tBCKA7pfcpkQ1lf58iQ==" + }, + "@types/range-parser": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.3.tgz", + "integrity": "sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA==" + }, + "@types/serve-static": { + "version": "1.13.8", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.8.tgz", + "integrity": "sha512-MoJhSQreaVoL+/hurAZzIm8wafFR6ajiTM1m4A0kv6AGeVBl4r4pOV8bGFrjjq1sGxDTnCoF8i22o0/aE5XCyA==", + "requires": { + "@types/mime": "*", + "@types/node": "*" + } + }, + "@types/ws": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-7.4.0.tgz", + "integrity": "sha512-Y29uQ3Uy+58bZrFLhX36hcI3Np37nqWE7ky5tjiDoy1GDZnIwVxS0CgF+s+1bXMzjKBFy+fqaRfb708iNzdinw==", + "requires": { + "@types/node": "*" + } + }, + "@wry/equality": { + "version": "0.1.11", + "resolved": "https://registry.npmjs.org/@wry/equality/-/equality-0.1.11.tgz", + "integrity": "sha512-mwEVBDUVODlsQQ5dfuLUS5/Tf7jqUKyhKYHmVi4fPB6bDMOfWvUPJmKgS1Z7Za/sOI3vzWt4+O7yCiL/70MogA==", + "requires": { + "tslib": "^1.9.3" + } + }, + "accepts": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", + "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==", + "requires": { + "mime-types": "~2.1.24", + "negotiator": "0.6.2" + } + }, + "apollo-cache-control": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/apollo-cache-control/-/apollo-cache-control-0.11.4.tgz", + "integrity": "sha512-FUKE8ASr8GxVq5rmky/tY8bsf++cleGT591lfLiqnPsP1fo3kAfgRfWA2QRHTCKFNlQxzUhVOEDv+PaysqiOjw==", + "requires": { + "apollo-server-env": "^2.4.5", + "apollo-server-plugin-base": "^0.10.2" + } + }, + "apollo-datasource": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/apollo-datasource/-/apollo-datasource-0.7.2.tgz", + "integrity": "sha512-ibnW+s4BMp4K2AgzLEtvzkjg7dJgCaw9M5b5N0YKNmeRZRnl/I/qBTQae648FsRKgMwTbRQIvBhQ0URUFAqFOw==", + "requires": { + "apollo-server-caching": "^0.5.2", + "apollo-server-env": "^2.4.5" + } + }, + "apollo-datasource-rest": { + "version": "0.9.5", + "resolved": "https://registry.npmjs.org/apollo-datasource-rest/-/apollo-datasource-rest-0.9.5.tgz", + "integrity": "sha512-/rtuQETxRIj6aQK1sVgSX9Nq4GkfdXT7sTaqdduc/EbmFIGs8xIZZh/QjFQ2HOqFrP7Qfno+eyK1lRChy6ILDA==", + "requires": { + "apollo-datasource": "^0.7.2", + "apollo-server-caching": "^0.5.2", + "apollo-server-env": "^2.4.5", + "apollo-server-errors": "^2.4.2", + "http-cache-semantics": "^4.0.0" + } + }, + "apollo-env": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/apollo-env/-/apollo-env-0.6.5.tgz", + "integrity": "sha512-jeBUVsGymeTHYWp3me0R2CZRZrFeuSZeICZHCeRflHTfnQtlmbSXdy5E0pOyRM9CU4JfQkKDC98S1YglQj7Bzg==", + "requires": { + "@types/node-fetch": "2.5.7", + "core-js": "^3.0.1", + "node-fetch": "^2.2.0", + "sha.js": "^2.4.11" + } + }, + "apollo-graphql": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/apollo-graphql/-/apollo-graphql-0.6.0.tgz", + "integrity": "sha512-BxTf5LOQe649e9BNTPdyCGItVv4Ll8wZ2BKnmiYpRAocYEXAVrQPWuSr3dO4iipqAU8X0gvle/Xu9mSqg5b7Qg==", + "requires": { + "apollo-env": "^0.6.5", + "lodash.sortby": "^4.7.0" + } + }, + "apollo-link": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/apollo-link/-/apollo-link-1.2.14.tgz", + "integrity": "sha512-p67CMEFP7kOG1JZ0ZkYZwRDa369w5PIjtMjvrQd/HnIV8FRsHRqLqK+oAZQnFa1DDdZtOtHTi+aMIW6EatC2jg==", + "requires": { + "apollo-utilities": "^1.3.0", + "ts-invariant": "^0.4.0", + "tslib": "^1.9.3", + "zen-observable-ts": "^0.8.21" + } + }, + "apollo-reporting-protobuf": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/apollo-reporting-protobuf/-/apollo-reporting-protobuf-0.6.1.tgz", + "integrity": "sha512-qr4DheFP154PGZsd93SSIS9RkqHnR5b6vT+eCloWjy3UIpY+yZ3cVLlttlIjYvOG4xTJ25XEwcHiAExatQo/7g==", + "requires": { + "@apollo/protobufjs": "^1.0.3" + } + }, + "apollo-server": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/apollo-server/-/apollo-server-2.19.0.tgz", + "integrity": "sha512-CchLtSwgm6NxQPvOXcMaxp8ckQT2ryLqdWIxjs2e+lCZ15tDsbqyPE+jVmqQKs9rsQNKnTwkMRdqmXqTciFJ8Q==", + "requires": { + "apollo-server-core": "^2.19.0", + "apollo-server-express": "^2.19.0", + "express": "^4.0.0", + "graphql-subscriptions": "^1.0.0", + "graphql-tools": "^4.0.0" + } + }, + "apollo-server-caching": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/apollo-server-caching/-/apollo-server-caching-0.5.2.tgz", + "integrity": "sha512-HUcP3TlgRsuGgeTOn8QMbkdx0hLPXyEJehZIPrcof0ATz7j7aTPA4at7gaiFHCo8gk07DaWYGB3PFgjboXRcWQ==", + "requires": { + "lru-cache": "^5.0.0" + } + }, + "apollo-server-core": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/apollo-server-core/-/apollo-server-core-2.19.0.tgz", + "integrity": "sha512-2aMKUVPyNbomJQaG2tkpfqvp1Tfgxgkdr7nX5zHudYNSzsPrHw+CcYlCbIVFFI/mTZsjoK9czNq1qerFRxZbJw==", + "requires": { + "@apollographql/apollo-tools": "^0.4.3", + "@apollographql/graphql-playground-html": "1.6.26", + "@types/graphql-upload": "^8.0.0", + "@types/ws": "^7.0.0", + "apollo-cache-control": "^0.11.4", + "apollo-datasource": "^0.7.2", + "apollo-graphql": "^0.6.0", + "apollo-reporting-protobuf": "^0.6.1", + "apollo-server-caching": "^0.5.2", + "apollo-server-env": "^2.4.5", + "apollo-server-errors": "^2.4.2", + "apollo-server-plugin-base": "^0.10.2", + "apollo-server-types": "^0.6.1", + "apollo-tracing": "^0.12.0", + "async-retry": "^1.2.1", + "fast-json-stable-stringify": "^2.0.0", + "graphql-extensions": "^0.12.6", + "graphql-tag": "^2.9.2", + "graphql-tools": "^4.0.0", + "graphql-upload": "^8.0.2", + "loglevel": "^1.6.7", + "lru-cache": "^5.0.0", + "sha.js": "^2.4.11", + "subscriptions-transport-ws": "^0.9.11", + "uuid": "^8.0.0", + "ws": "^6.0.0" + } + }, + "apollo-server-env": { + "version": "2.4.5", + "resolved": "https://registry.npmjs.org/apollo-server-env/-/apollo-server-env-2.4.5.tgz", + "integrity": "sha512-nfNhmGPzbq3xCEWT8eRpoHXIPNcNy3QcEoBlzVMjeglrBGryLG2LXwBSPnVmTRRrzUYugX0ULBtgE3rBFNoUgA==", + "requires": { + "node-fetch": "^2.1.2", + "util.promisify": "^1.0.0" + } + }, + "apollo-server-errors": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/apollo-server-errors/-/apollo-server-errors-2.4.2.tgz", + "integrity": "sha512-FeGxW3Batn6sUtX3OVVUm7o56EgjxDlmgpTLNyWcLb0j6P8mw9oLNyAm3B+deHA4KNdNHO5BmHS2g1SJYjqPCQ==" + }, + "apollo-server-express": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/apollo-server-express/-/apollo-server-express-2.19.0.tgz", + "integrity": "sha512-3rgSrTme1SlLoecAYtSa8ThH6vYvz29QecgZCigq5Vdc6bFP2SZrCk0ls6BAdD8OZbVKUtizzRxd0yd/uREPAw==", + "requires": { + "@apollographql/graphql-playground-html": "1.6.26", + "@types/accepts": "^1.3.5", + "@types/body-parser": "1.19.0", + "@types/cors": "2.8.8", + "@types/express": "4.17.7", + "@types/express-serve-static-core": "4.17.13", + "accepts": "^1.3.5", + "apollo-server-core": "^2.19.0", + "apollo-server-types": "^0.6.1", + "body-parser": "^1.18.3", + "cors": "^2.8.4", + "express": "^4.17.1", + "graphql-subscriptions": "^1.0.0", + "graphql-tools": "^4.0.0", + "parseurl": "^1.3.2", + "subscriptions-transport-ws": "^0.9.16", + "type-is": "^1.6.16" + }, + "dependencies": { + "@types/express": { + "version": "4.17.7", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.7.tgz", + "integrity": "sha512-dCOT5lcmV/uC2J9k0rPafATeeyz+99xTt54ReX11/LObZgfzJqZNcW27zGhYyX+9iSEGXGt5qLPwRSvBZcLvtQ==", + "requires": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "*", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "@types/express-serve-static-core": { + "version": "4.17.13", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.13.tgz", + "integrity": "sha512-RgDi5a4nuzam073lRGKTUIaL3eF2+H7LJvJ8eUnCI0wA6SNjXc44DCmWNiTLs/AZ7QlsFWZiw/gTG3nSQGL0fA==", + "requires": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*" + } + } + } + }, + "apollo-server-plugin-base": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/apollo-server-plugin-base/-/apollo-server-plugin-base-0.10.2.tgz", + "integrity": "sha512-uM5uL1lOxbXdgvt/aEIbgs40fV9xA45Y3Mmh0VtQ/ddqq0MXR5aG92nnf8rM+URarBCUfxKJKaYzJJ/CXAnEdA==", + "requires": { + "apollo-server-types": "^0.6.1" + } + }, + "apollo-server-types": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/apollo-server-types/-/apollo-server-types-0.6.1.tgz", + "integrity": "sha512-IEQ37aYvMLiTUzsySVLOSuvvhxuyYdhI05f3cnH6u2aN1HgGp7vX6bg+U3Ue8wbHfdcifcGIk5UEU+Q+QO6InA==", + "requires": { + "apollo-reporting-protobuf": "^0.6.1", + "apollo-server-caching": "^0.5.2", + "apollo-server-env": "^2.4.5" + } + }, + "apollo-tracing": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/apollo-tracing/-/apollo-tracing-0.12.0.tgz", + "integrity": "sha512-cMUYGE6mOEwb9HDqhf4fiPEo2JMhjPIqEprAQEC57El76avRpRig5NM0bnqMZcYJZR5QmLlNcttNccOwf9WrNg==", + "requires": { + "apollo-server-env": "^2.4.5", + "apollo-server-plugin-base": "^0.10.2" + } + }, + "apollo-utilities": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/apollo-utilities/-/apollo-utilities-1.3.4.tgz", + "integrity": "sha512-pk2hiWrCXMAy2fRPwEyhvka+mqwzeP60Jr1tRYi5xru+3ko94HI9o6lK0CT33/w4RDlxWchmdhDCrvdr+pHCig==", + "requires": { + "@wry/equality": "^0.1.2", + "fast-json-stable-stringify": "^2.0.0", + "ts-invariant": "^0.4.0", + "tslib": "^1.10.0" + } + }, + "array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" + }, + "async-limiter": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", + "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==" + }, + "async-retry": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/async-retry/-/async-retry-1.3.1.tgz", + "integrity": "sha512-aiieFW/7h3hY0Bq5d+ktDBejxuwR78vRu9hDUdR8rNhSaQ29VzPL4AoIRG7D/c7tdenwOcKvgPM6tIxB3cB6HA==", + "requires": { + "retry": "0.12.0" + } + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" + }, + "backo2": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz", + "integrity": "sha1-MasayLEpNjRj41s+u2n038+6eUc=" + }, + "body-parser": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", + "integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==", + "requires": { + "bytes": "3.1.0", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "~1.1.2", + "http-errors": "1.7.2", + "iconv-lite": "0.4.24", + "on-finished": "~2.3.0", + "qs": "6.7.0", + "raw-body": "2.4.0", + "type-is": "~1.6.17" + }, + "dependencies": { + "http-errors": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", + "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.1", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + }, + "setprototypeof": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", + "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" + } + } + }, + "busboy": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-0.3.1.tgz", + "integrity": "sha512-y7tTxhGKXcyBxRKAni+awqx8uqaJKrSFSNFSeRG5CsWNdmy2BIK+6VGWEW7TZnIO/533mtMEA4rOevQV815YJw==", + "requires": { + "dicer": "0.3.0" + } + }, + "bytes": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", + "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==" + }, + "call-bind": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.0.tgz", + "integrity": "sha512-AEXsYIyyDY3MCzbwdhzG3Jx1R0J2wetQyUynn6dYHAO+bg8l1k7jwZtRv4ryryFs7EP+NDlikJlVe59jr0cM2w==", + "requires": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.0" + } + }, + "cluster-key-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.0.tgz", + "integrity": "sha512-2Nii8p3RwAPiFwsnZvukotvow2rIHM+yQ6ZcBXGHdniadkYGZYiGmkHJIbZPIV9nfv7m/U1IPMVVcAhoWFeklw==" + }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" + }, + "content-disposition": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", + "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==", + "requires": { + "safe-buffer": "5.1.2" + }, + "dependencies": { + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + } + } + }, + "content-type": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" + }, + "cookie": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", + "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==" + }, + "cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" + }, + "core-js": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.8.0.tgz", + "integrity": "sha512-W2VYNB0nwQQE7tKS7HzXd7r2y/y2SVJl4ga6oH/dnaLFzM0o2lB2P3zCkWj5Wc/zyMYjtgd5Hmhk0ObkQFZOIA==" + }, + "cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "requires": { + "object-assign": "^4", + "vary": "^1" + } + }, + "cssfilter": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/cssfilter/-/cssfilter-0.0.10.tgz", + "integrity": "sha1-xtJnJjKi5cg+AT5oZKQs6N79IK4=" + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "define-properties": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", + "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", + "requires": { + "object-keys": "^1.0.12" + } + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" + }, + "denque": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/denque/-/denque-1.4.1.tgz", + "integrity": "sha512-OfzPuSZKGcgr96rf1oODnfjqBFmr1DVoc/TrItj3Ohe0Ah1C5WX5Baquw/9U9KovnQ88EqmJbD66rKYUQYN1tQ==" + }, + "depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" + }, + "deprecated-decorator": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/deprecated-decorator/-/deprecated-decorator-0.1.6.tgz", + "integrity": "sha1-AJZjF7ehL+kvPMgx91g68ym4bDc=" + }, + "destroy": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", + "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" + }, + "dicer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/dicer/-/dicer-0.3.0.tgz", + "integrity": "sha512-MdceRRWqltEG2dZqO769g27N/3PXfcKl04VhYnBlo2YhH7zPi88VebsjTKclaOyiuMaGU72hTfw3VkUitGcVCA==", + "requires": { + "streamsearch": "0.1.2" + } + }, + "ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" + }, + "encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" + }, + "es-abstract": { + "version": "1.17.7", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.7.tgz", + "integrity": "sha512-VBl/gnfcJ7OercKA9MVaegWsBHFjV492syMudcnQZvt/Dw8ezpcOHYZXa/J96O8vx+g4x65YKhxOwDUh63aS5g==", + "requires": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.2.2", + "is-regex": "^1.1.1", + "object-inspect": "^1.8.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.1", + "string.prototype.trimend": "^1.0.1", + "string.prototype.trimstart": "^1.0.1" + } + }, + "es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "requires": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + } + }, + "escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" + }, + "etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" + }, + "eventemitter3": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.1.2.tgz", + "integrity": "sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q==" + }, + "express": { + "version": "4.17.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz", + "integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==", + "requires": { + "accepts": "~1.3.7", + "array-flatten": "1.1.1", + "body-parser": "1.19.0", + "content-disposition": "0.5.3", + "content-type": "~1.0.4", + "cookie": "0.4.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "~1.1.2", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.1.2", + "fresh": "0.5.2", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.5", + "qs": "6.7.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.1.2", + "send": "0.17.1", + "serve-static": "1.14.1", + "setprototypeof": "1.1.1", + "statuses": "~1.5.0", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "dependencies": { + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "setprototypeof": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", + "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" + } + } + }, + "fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" + }, + "finalhandler": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", + "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", + "requires": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "statuses": "~1.5.0", + "unpipe": "~1.0.0" + } + }, + "form-data": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.0.tgz", + "integrity": "sha512-CKMFDglpbMi6PyN+brwB9Q/GOw0eAnsrEZDgcsH5Krhz5Od/haKHAX0NmQfha2zPPz0JpWzA7GJHGSnvCRLWsg==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + }, + "forwarded": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", + "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=" + }, + "fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" + }, + "fs-capacitor": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/fs-capacitor/-/fs-capacitor-2.0.4.tgz", + "integrity": "sha512-8S4f4WsCryNw2mJJchi46YgB6CR5Ze+4L1h8ewl9tEpL4SJ3ZO+c/bS4BWhB8bK+O3TMqhuZarTitd0S0eh2pA==" + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + }, + "get-intrinsic": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.0.1.tgz", + "integrity": "sha512-ZnWP+AmS1VUaLgTRy47+zKtjTxz+0xMpx3I52i+aalBK1QP19ggLF3Db89KJX7kjfOfP2eoa01qc++GwPgufPg==", + "requires": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1" + } + }, + "graphql": { + "version": "15.4.0", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-15.4.0.tgz", + "integrity": "sha512-EB3zgGchcabbsU9cFe1j+yxdzKQKAbGUWRb13DsrsMN1yyfmmIq+2+L5MqVWcDCE4V89R5AyUOi7sMOGxdsYtA==" + }, + "graphql-extensions": { + "version": "0.12.6", + "resolved": "https://registry.npmjs.org/graphql-extensions/-/graphql-extensions-0.12.6.tgz", + "integrity": "sha512-EUNw+OIRXYTPxToSoJjhJvS5aGa94KkdkZnL1I9DCZT64/+rzQNeLeGj+goj2RYuYvoQe1Bmcx0CNZ1GqwBhng==", + "requires": { + "@apollographql/apollo-tools": "^0.4.3", + "apollo-server-env": "^2.4.5", + "apollo-server-types": "^0.6.1" + } + }, + "graphql-subscriptions": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/graphql-subscriptions/-/graphql-subscriptions-1.1.0.tgz", + "integrity": "sha512-6WzlBFC0lWmXJbIVE8OgFgXIP4RJi3OQgTPa0DVMsDXdpRDjTsM1K9wfl5HSYX7R87QAGlvcv2Y4BIZa/ItonA==", + "requires": { + "iterall": "^1.2.1" + } + }, + "graphql-tag": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/graphql-tag/-/graphql-tag-2.11.0.tgz", + "integrity": "sha512-VmsD5pJqWJnQZMUeRwrDhfgoyqcfwEkvtpANqcoUG8/tOLkwNgU9mzub/Mc78OJMhHjx7gfAMTxzdG43VGg3bA==" + }, + "graphql-tools": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/graphql-tools/-/graphql-tools-4.0.8.tgz", + "integrity": "sha512-MW+ioleBrwhRjalKjYaLQbr+920pHBgy9vM/n47sswtns8+96sRn5M/G+J1eu7IMeKWiN/9p6tmwCHU7552VJg==", + "requires": { + "apollo-link": "^1.2.14", + "apollo-utilities": "^1.0.1", + "deprecated-decorator": "^0.1.6", + "iterall": "^1.1.3", + "uuid": "^3.1.0" + }, + "dependencies": { + "uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==" + } + } + }, + "graphql-upload": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/graphql-upload/-/graphql-upload-8.1.0.tgz", + "integrity": "sha512-U2OiDI5VxYmzRKw0Z2dmfk0zkqMRaecH9Smh1U277gVgVe9Qn+18xqf4skwr4YJszGIh7iQDZ57+5ygOK9sM/Q==", + "requires": { + "busboy": "^0.3.1", + "fs-capacitor": "^2.0.4", + "http-errors": "^1.7.3", + "object-path": "^0.11.4" + } + }, + "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-symbols": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz", + "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==" + }, + "http-cache-semantics": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz", + "integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==" + }, + "http-errors": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.0.tgz", + "integrity": "sha512-4I8r0C5JDhT5VkvI47QktDW75rNlGVsUf/8hzjCC/wkWI/jdTRmBb9aI7erSG82r1bjKY3F6k28WnsVxB1C73A==", + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" + } + }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "ioredis": { + "version": "4.19.2", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-4.19.2.tgz", + "integrity": "sha512-SZSIwMrbd96b7rJvJwyTWSP6XQ0m1kAIIqBnwglJKrIJ6na7TeY4F2EV2vDY0xm/fLrUY8cEg81dR7kVFt2sKA==", + "requires": { + "cluster-key-slot": "^1.1.0", + "debug": "^4.1.1", + "denque": "^1.1.0", + "lodash.defaults": "^4.2.0", + "lodash.flatten": "^4.4.0", + "p-map": "^2.1.0", + "redis-commands": "1.6.0", + "redis-errors": "^1.2.0", + "redis-parser": "^3.0.0", + "standard-as-callback": "^2.0.1" + }, + "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==" + } + } + }, + "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==" + }, + "is-callable": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.2.tgz", + "integrity": "sha512-dnMqspv5nU3LoewK2N/y7KLtxtakvTuaCsU9FU50/QDmdbHNy/4/JuRtMHqRU22o3q+W89YQndQEeCVwK+3qrA==" + }, + "is-date-object": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.2.tgz", + "integrity": "sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g==" + }, + "is-regex": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.1.tgz", + "integrity": "sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg==", + "requires": { + "has-symbols": "^1.0.1" + } + }, + "is-symbol": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.3.tgz", + "integrity": "sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==", + "requires": { + "has-symbols": "^1.0.1" + } + }, + "iterall": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/iterall/-/iterall-1.3.0.tgz", + "integrity": "sha512-QZ9qOMdF+QLHxy1QIpUHUU1D5pS2CG2P69LF6L6CPjPYA/XMOmKV3PZpawHoAjHNyB0swdVTRxdYT4tbBbxqwg==" + }, + "lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw=" + }, + "lodash.flatten": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", + "integrity": "sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8=" + }, + "lodash.sortby": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", + "integrity": "sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=" + }, + "loglevel": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.7.1.tgz", + "integrity": "sha512-Hesni4s5UkWkwCGJMQGAh71PaLUmKFM60dHvq0zi/vDhhrzuk+4GgNbTXJ12YYQJn6ZKBDNIjYcuQGKudvqrIw==" + }, + "long": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", + "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==" + }, + "lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "requires": { + "yallist": "^3.0.2" + } + }, + "media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" + }, + "merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" + }, + "methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" + }, + "mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" + }, + "mime-db": { + "version": "1.44.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.44.0.tgz", + "integrity": "sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg==" + }, + "mime-types": { + "version": "2.1.27", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.27.tgz", + "integrity": "sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w==", + "requires": { + "mime-db": "1.44.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "negotiator": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", + "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==" + }, + "node-fetch": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", + "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==" + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" + }, + "object-inspect": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.8.0.tgz", + "integrity": "sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA==" + }, + "object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==" + }, + "object-path": { + "version": "0.11.5", + "resolved": "https://registry.npmjs.org/object-path/-/object-path-0.11.5.tgz", + "integrity": "sha512-jgSbThcoR/s+XumvGMTMf81QVBmah+/Q7K7YduKeKVWL7N111unR2d6pZZarSk6kY/caeNxUDyxOvMWyzoU2eg==" + }, + "object.assign": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz", + "integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==", + "requires": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3", + "has-symbols": "^1.0.1", + "object-keys": "^1.1.1" + } + }, + "object.getownpropertydescriptors": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.0.tgz", + "integrity": "sha512-Z53Oah9A3TdLoblT7VKJaTDdXdT+lQO+cNpKVnya5JDe9uLvzu1YyY1yFDFrcxrlRgWrEFH0jJtD/IbuwjcEVg==", + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.0-next.1" + } + }, + "on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", + "requires": { + "ee-first": "1.1.1" + } + }, + "p-map": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-2.1.0.tgz", + "integrity": "sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==" + }, + "parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" + }, + "path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" + }, + "proxy-addr": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.6.tgz", + "integrity": "sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw==", + "requires": { + "forwarded": "~0.1.2", + "ipaddr.js": "1.9.1" + } + }, + "qs": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", + "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==" + }, + "range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" + }, + "raw-body": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz", + "integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==", + "requires": { + "bytes": "3.1.0", + "http-errors": "1.7.2", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "dependencies": { + "http-errors": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", + "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.1", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + }, + "setprototypeof": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", + "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" + } + } + }, + "redis-commands": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/redis-commands/-/redis-commands-1.6.0.tgz", + "integrity": "sha512-2jnZ0IkjZxvguITjFTrGiLyzQZcTvaw8DAaCXxZq/dsHXz7KfMQ3OUJy7Tz9vnRtZRVz6VRCPDvruvU8Ts44wQ==" + }, + "redis-errors": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", + "integrity": "sha1-62LSrbFeTq9GEMBK/hUpOEJQq60=" + }, + "redis-parser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz", + "integrity": "sha1-tm2CjNyv5rS4pCin3vTGvKwxyLQ=", + "requires": { + "redis-errors": "^1.0.0" + } + }, + "retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs=" + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "send": { + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz", + "integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==", + "requires": { + "debug": "2.6.9", + "depd": "~1.1.2", + "destroy": "~1.0.4", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "~1.7.2", + "mime": "1.6.0", + "ms": "2.1.1", + "on-finished": "~2.3.0", + "range-parser": "~1.2.1", + "statuses": "~1.5.0" + }, + "dependencies": { + "http-errors": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.3.tgz", + "integrity": "sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw==", + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.4", + "setprototypeof": "1.1.1", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" + } + }, + "ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" + }, + "setprototypeof": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", + "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" + } + } + }, + "serve-static": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz", + "integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==", + "requires": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.17.1" + } + }, + "setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "sha.js": { + "version": "2.4.11", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", + "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", + "requires": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "standard-as-callback": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.0.1.tgz", + "integrity": "sha512-NQOxSeB8gOI5WjSaxjBgog2QFw55FV8TkS6Y07BiB3VJ8xNTvUYm0wl0s8ObgQ5NhdpnNfigMIKjgPESzgr4tg==" + }, + "statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" + }, + "streamsearch": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-0.1.2.tgz", + "integrity": "sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo=" + }, + "string.prototype.trimend": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.3.tgz", + "integrity": "sha512-ayH0pB+uf0U28CtjlLvL7NaohvR1amUvVZk+y3DYb0Ey2PUV5zPkkKy9+U1ndVEIXO8hNg18eIv9Jntbii+dKw==", + "requires": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3" + } + }, + "string.prototype.trimstart": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.3.tgz", + "integrity": "sha512-oBIBUy5lea5tt0ovtOFiEQaBkoBBkyJhZXzJYrSmDo5IUUqbOPvVezuRs/agBIdZ2p2Eo1FD6bD9USyBLfl3xg==", + "requires": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3" + } + }, + "subscriptions-transport-ws": { + "version": "0.9.18", + "resolved": "https://registry.npmjs.org/subscriptions-transport-ws/-/subscriptions-transport-ws-0.9.18.tgz", + "integrity": "sha512-tztzcBTNoEbuErsVQpTN2xUNN/efAZXyCyL5m3x4t6SKrEiTL2N8SaKWBFWM4u56pL79ULif3zjyeq+oV+nOaA==", + "requires": { + "backo2": "^1.0.2", + "eventemitter3": "^3.1.0", + "iterall": "^1.2.1", + "symbol-observable": "^1.0.4", + "ws": "^5.2.0" + }, + "dependencies": { + "ws": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-5.2.2.tgz", + "integrity": "sha512-jaHFD6PFv6UgoIVda6qZllptQsMlDEJkTQcybzzXDYM1XO9Y8em691FGMPmM46WGyLU4z9KMgQN+qrux/nhlHA==", + "requires": { + "async-limiter": "~1.0.0" + } + } + } + }, + "symbol-observable": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz", + "integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==" + }, + "toidentifier": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", + "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==" + }, + "ts-invariant": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/ts-invariant/-/ts-invariant-0.4.4.tgz", + "integrity": "sha512-uEtWkFM/sdZvRNNDL3Ehu4WVpwaulhwQszV8mrtcdeE8nN00BV9mAmQ88RkrBhFgl9gMgvjJLAQcZbnPXI9mlA==", + "requires": { + "tslib": "^1.9.3" + } + }, + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + }, + "type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "requires": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + } + }, + "unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" + }, + "util.promisify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/util.promisify/-/util.promisify-1.0.1.tgz", + "integrity": "sha512-g9JpC/3He3bm38zsLupWryXHoEcS22YHthuPQSJdMy6KNrzIRzWqcsHzD/WUnqe45whVou4VIsPew37DoXWNrA==", + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.2", + "has-symbols": "^1.0.1", + "object.getownpropertydescriptors": "^2.1.0" + } + }, + "utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" + }, + "uuid": { + "version": "8.3.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.1.tgz", + "integrity": "sha512-FOmRr+FmWEIG8uhZv6C2bTgEVXsHk08kE7mPlrBbEe+c3r9pjceVPgupIfNIhc4yx55H69OXANrUaSuu9eInKg==" + }, + "vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" + }, + "ws": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.1.tgz", + "integrity": "sha512-GIyAXC2cB7LjvpgMt9EKS2ldqr0MTrORaleiOno6TweZ6r3TKtoFQWay/2PceJ3RuBasOHzXNn5Lrw1X0bEjqA==", + "requires": { + "async-limiter": "~1.0.0" + } + }, + "xss": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/xss/-/xss-1.0.8.tgz", + "integrity": "sha512-3MgPdaXV8rfQ/pNn16Eio6VXYPTkqwa0vc7GkiymmY/DqR1SE/7VPAAVZz1GJsJFrllMYO3RHfEaiUGjab6TNw==", + "requires": { + "commander": "^2.20.3", + "cssfilter": "0.0.10" + } + }, + "yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" + }, + "zen-observable": { + "version": "0.8.15", + "resolved": "https://registry.npmjs.org/zen-observable/-/zen-observable-0.8.15.tgz", + "integrity": "sha512-PQ2PC7R9rslx84ndNBZB/Dkv8V8fZEpk83RLgXtYd0fwUgEjseMn1Dgajh2x6S8QbZAFa9p2qVCEuYZNgve0dQ==" + }, + "zen-observable-ts": { + "version": "0.8.21", + "resolved": "https://registry.npmjs.org/zen-observable-ts/-/zen-observable-ts-0.8.21.tgz", + "integrity": "sha512-Yj3yXweRc8LdRMrCC8nIc4kkjWecPAUVh0TI0OUrWXx6aX790vLcDlWca6I4vsyCGH3LpWxq0dJRcMOFoVqmeg==", + "requires": { + "tslib": "^1.9.3", + "zen-observable": "^0.8.0" + } + } + } +} diff --git a/server/orchestrators/graphqlOrchestrator/package.json b/server/orchestrators/graphqlOrchestrator/package.json new file mode 100644 index 0000000..cd52749 --- /dev/null +++ b/server/orchestrators/graphqlOrchestrator/package.json @@ -0,0 +1,19 @@ +{ + "name": "graphqlOrchestrator", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "start": "nodemon src", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "apollo-datasource-rest": "^0.9.5", + "apollo-server": "^2.19.0", + "graphql": "^15.4.0", + "ioredis": "^4.19.2" + } +} diff --git a/server/orchestrators/graphqlOrchestrator/src/dataSources/logsAPI.js b/server/orchestrators/graphqlOrchestrator/src/dataSources/logsAPI.js new file mode 100644 index 0000000..ceb28f2 --- /dev/null +++ b/server/orchestrators/graphqlOrchestrator/src/dataSources/logsAPI.js @@ -0,0 +1,23 @@ +const { RESTDataSource } = require('apollo-datasource-rest'); + +class LogsAPI extends RESTDataSource { + constructor() { + super(); + this.baseURL = 'http://localhost:3002'; + } + + async createLog(log) { + return this.post('/logs', log); + } + async readLogs() { + return this.get('/logs'); + } + async deleteLog(id) { + return this.delete(`/logs/${id}`); + } + async resetLogs() { + return this.delete('/logs'); + } +} + +module.exports = LogsAPI; diff --git a/server/orchestrators/graphqlOrchestrator/src/dataSources/usersAPI.js b/server/orchestrators/graphqlOrchestrator/src/dataSources/usersAPI.js new file mode 100644 index 0000000..beacaf4 --- /dev/null +++ b/server/orchestrators/graphqlOrchestrator/src/dataSources/usersAPI.js @@ -0,0 +1,104 @@ +const { RESTDataSource } = require('apollo-datasource-rest'); + +class UsersAPI extends RESTDataSource { + constructor() { + super(); + this.baseURL = 'http://localhost:3001'; + } + + // handle user registration, verification, and login features : + async registerUser(user) { + return this.post('/users/register', user); + } + async verifyUser(token) { + return this.get(`/users/verify?token=${token}`); + } + async loginUser(user) { + return this.post('/users/login', user); + } + + // handle post features : + async createPost(post, token) { + return this.post('/posts', post, { + headers: { + access_token: token, + }, + }); + } + async readPosts() { + return this.get('/posts'); + } + async searchPosts(title, sort, order) { + return this.get(`/posts/search?title=${title}&sort=${sort}&order=${order}`); + } + async findPostById(id) { + return this.get(`/posts/${id}`); + } + async findPostsByUserId(id) { + return this.get(`/posts/user/${id}`); + } + async updatePost(id, post, token) { + return this.put(`/posts/${id}`, post, { + headers: { + access_token: token, + }, + }); + } + async deletePost(id, token) { + return this.delete(`/posts/${id}`, null, { + headers: { + access_token: token, + }, + }); + } + + // handle like-unlike features : + async createLike(like, token) { + return this.post('/likes', like, { + headers: { + access_token: token, + }, + }); + } + async deleteLike(id, token) { + return this.delete(`/likes/${id}`, null, { + headers: { + access_token: token, + }, + }); + } + + // handle comment features : + async createComment(comment, token) { + return this.post('/comments', comment, { + headers: { + access_token: token, + }, + }); + } + async deleteComment(id, token) { + return this.delete(`/comments/${id}`, null, { + headers: { + access_token: token, + }, + }); + } + + // handle sub comment features : + async createSubComment(comment, token) { + return this.post('/sub-comments', comment, { + headers: { + access_token: token, + }, + }); + } + async deleteSubComment(id, token) { + return this.delete(`/sub-comments/${id}`, null, { + headers: { + access_token: token, + }, + }); + } +} + +module.exports = UsersAPI; diff --git a/server/orchestrators/graphqlOrchestrator/src/index.js b/server/orchestrators/graphqlOrchestrator/src/index.js new file mode 100644 index 0000000..8129468 --- /dev/null +++ b/server/orchestrators/graphqlOrchestrator/src/index.js @@ -0,0 +1,23 @@ +'use strict'; + +const { ApolloServer } = require('apollo-server'); +const schema = require('./schema'); +const resolvers = require('./resolvers'); +const UsersAPI = require('./dataSources/usersAPI'); + +const server = new ApolloServer({ + typeDefs: schema, + resolvers, + dataSources() { + return { + usersAPI: new UsersAPI(), + }; + }, +}); + +server + .listen(3000) + .then(({ url }) => { + console.log(`graphqlOrchestrator running at ${url}`); + }) + .catch(err => console.log(err)); diff --git a/server/orchestrators/graphqlOrchestrator/src/resolvers/index.js b/server/orchestrators/graphqlOrchestrator/src/resolvers/index.js new file mode 100644 index 0000000..81a9f86 --- /dev/null +++ b/server/orchestrators/graphqlOrchestrator/src/resolvers/index.js @@ -0,0 +1,54 @@ +const { + posts, + search_posts, + post_by_id, + posts_by_user_id, + logs, +} = require('./queries'); + +const { + register_user, + verify_user, + login_user, + create_post, + update_post, + delete_post, + create_like, + delete_like, + create_comment, + delete_comment, + create_sub_comment, + delete_sub_comment, + create_log, + delete_log, + reset_logs, +} = require('./mutations'); + +const resolvers = { + Query: { + posts, + search_posts, + post_by_id, + posts_by_user_id, + logs, + }, + Mutation: { + register_user, + verify_user, + login_user, + create_post, + update_post, + delete_post, + create_like, + delete_like, + create_comment, + delete_comment, + create_sub_comment, + delete_sub_comment, + create_log, + delete_log, + reset_logs, + }, +}; + +module.exports = resolvers; diff --git a/server/orchestrators/graphqlOrchestrator/src/resolvers/mutations/index.js b/server/orchestrators/graphqlOrchestrator/src/resolvers/mutations/index.js new file mode 100644 index 0000000..575b035 --- /dev/null +++ b/server/orchestrators/graphqlOrchestrator/src/resolvers/mutations/index.js @@ -0,0 +1,77 @@ +const Redis = require('ioredis'); +const redis = new Redis(); + +// handle user registration, verification, and login features : +async function register_user(_, user, { dataSources }) { + return dataSources.usersAPI.registerUser(user); +} +async function verify_user(_, { token }, { dataSources }) { + return dataSources.usersAPI.verifyUser(token); +} +async function login_user(_, user, { dataSources }) { + return dataSources.usersAPI.loginUser(user); +} + +// handle post features : +async function create_post(_, { title, content, access_token }, { dataSources }) { + return dataSources.usersAPI.createPost({ title, content }, access_token); +} +async function update_post(_, { id, title, content, access_token }, { dataSources }) { + return dataSources.usersAPI.updatePost(id, { title, content }, access_token); +} +async function delete_post(_, { id, access_token }, { dataSources }) { + return dataSources.usersAPI.deletePost(id, access_token); +} + +// handle like-unlike features : +async function create_like(_, { PostId, access_token }, { dataSources }) { + return dataSources.usersAPI.createLike({ PostId }, access_token); +} +async function delete_like(_, { id, access_token }, { dataSources }) { + return dataSources.usersAPI.deleteLike(id, access_token); +} + +// handle comment features : +async function create_comment(_, { content, PostId, access_token }, { dataSources }) { + return dataSources.usersAPI.createComment({ content, PostId }, access_token); +} +async function delete_comment(_, { id, access_token }, { dataSources }) { + return dataSources.usersAPI.deleteComment(id, access_token); +} + +// handle sub comment features : +async function create_sub_comment(_, { content, CommentId, access_token }, { dataSources }) { + return dataSources.usersAPI.createSubComment({ content, CommentId }, access_token); +} +async function delete_sub_comment(_, { id, access_token }, { dataSources }) { + return dataSources.usersAPI.deleteSubComment(id, access_token); +} + +// handle log features : +async function create_log(_, log, { dataSources }) { + return dataSources.logsAPI.createLog(log); +} +async function delete_log(_, { id }, { dataSources }) { + return dataSources.logsAPI.deleteLog(id); +} +async function reset_logs(_, __, { dataSources }) { + return dataSources.logsAPI.resetLogs(); +} + +module.exports = { + register_user, + verify_user, + login_user, + create_post, + update_post, + delete_post, + create_like, + delete_like, + create_comment, + delete_comment, + create_sub_comment, + delete_sub_comment, + create_log, + delete_log, + reset_logs, +}; diff --git a/server/orchestrators/graphqlOrchestrator/src/resolvers/queries/index.js b/server/orchestrators/graphqlOrchestrator/src/resolvers/queries/index.js new file mode 100644 index 0000000..916aaa9 --- /dev/null +++ b/server/orchestrators/graphqlOrchestrator/src/resolvers/queries/index.js @@ -0,0 +1,43 @@ +const Redis = require('ioredis'); +const redis = new Redis(); + +// handle post features : +async function posts(_, __, { dataSources }) { + const cached_posts = await redis.get('posts'); + if (cached_posts) { + return JSON.parse(cached_posts); + } else { + const posts = await dataSources.usersAPI.readPosts(); + await redis.set('posts', JSON.stringify(posts)); + return posts; + } +} +async function search_posts(_, { title, sort, order }, { dataSources }) { + return dataSources.usersAPI.searchPosts(title, sort, order); +} +async function post_by_id(_, { id }, { dataSources }) { + return dataSources.usersAPI.findPostById(id); +} +async function posts_by_user_id(_, { id }, { dataSources }) { + return dataSources.usersAPI.findPostsByUserId(id); +} + +// handle log feature : +async function logs(_, __, { dataSources }) { + const cached_logs = await redis.get('logs'); + if (cached_logs) { + return JSON.parse(cached_logs); + } else { + const logs = await dataSources.logsAPI.readLogs(); + await redis.set('logs', JSON.stringify(logs)); + return logs; + } +} + +module.exports = { + posts, + search_posts, + post_by_id, + posts_by_user_id, + logs, +}; diff --git a/server/orchestrators/graphqlOrchestrator/src/schema/index.js b/server/orchestrators/graphqlOrchestrator/src/schema/index.js new file mode 100644 index 0000000..10bb4e0 --- /dev/null +++ b/server/orchestrators/graphqlOrchestrator/src/schema/index.js @@ -0,0 +1,12 @@ +const { gql } = require('apollo-server'); +const models = require('./models'); +const queries = require('./queries'); +const mutations = require('./mutations'); + +const schema = gql` + ${models} + ${queries} + ${mutations} +`; + +module.exports = schema; diff --git a/server/orchestrators/graphqlOrchestrator/src/schema/models/index.js b/server/orchestrators/graphqlOrchestrator/src/schema/models/index.js new file mode 100644 index 0000000..024c80f --- /dev/null +++ b/server/orchestrators/graphqlOrchestrator/src/schema/models/index.js @@ -0,0 +1,128 @@ +const models = ` + type User { + id: ID + username: String + email: String + password: String + status: String + createdAt: String + updatedAt: String + } + + type UserOutputRegister { + message: String + verification_token: String + data: User + } + + type UserOutputVerify { + message: String + data: User + } + + type UserOutputLogin { + message: String + access_token: String + user_id: Int + } + + type Post { + id: ID + title: String + content: String + UserId: Int + createdAt: String + updatedAt: String + User: User + Likes: [Like] + Comments: [Comment] + } + + type PostOutputSearch { + count: Int + data: [Post] + } + + type PostOutputDelete { + message: String + deleted_post: Post + } + + type Like { + id: ID + PostId: Int + UserId: Int + createdAt: String + updatedAt: String + } + + type LikeOutputDelete { + message: String + deleted_like: Like + } + + type Comment { + id: ID + content: String + PostId: Int + UserId: Int + createdAt: String + updatedAt: String + SubComments: [SubComment] + } + + type CommentOutputDelete { + message: String + deleted_comment: Comment + } + + type SubComment { + id: ID + content: String + CommentId: Int + UserId: Int + createdAt: String + updatedAt: String + } + + type SubCommentOutputDelete { + message: String + deleted_sub_comment: SubComment + } + + type Log { + _id: ID + path: String + user_detail: UserDetail + api_access_time: Float + request_object: RequestObject + response_object: ResponseObject + createdAt: String + updatedAt: String + } + + type UserDetail { + username: String + email: String + status: String + } + + type RequestObject { + method: String + } + + type ResponseObject { + status_code: Int + } + + type LogOutputDelete { + message: String + deleted_log: Log + } + + type LogOutputReset { + message: String + } +`; + +module.exports = models; diff --git a/server/orchestrators/graphqlOrchestrator/src/schema/mutations/index.js b/server/orchestrators/graphqlOrchestrator/src/schema/mutations/index.js new file mode 100644 index 0000000..e0cb3b2 --- /dev/null +++ b/server/orchestrators/graphqlOrchestrator/src/schema/mutations/index.js @@ -0,0 +1,86 @@ +const mutations = ` + type Mutation { + register_user( + username: String + email: String + password: String + ): UserOutputRegister + + verify_user( + token: String + ): UserOutputVerify + + login_user( + email: String + password: String + ): UserOutputLogin + + create_post( + title: String + content: String + access_token: String + ): Post + + update_post( + id: Int, + title: String + content: String + access_token: String + ): Post + + delete_post( + id: Int + access_token: String + ): PostOutputDelete + + create_like( + PostId: Int + access_token: String + ): Like + + delete_like( + id: Int + access_token: String + ): LikeOutputDelete + + create_comment( + content: String + PostId: Int + access_token: String + ): Comment + + delete_comment( + id: Int + access_token: String + ): CommentOutputDelete + + create_sub_comment( + content: String + CommentId: Int + access_token: String + ): SubComment + + delete_sub_comment( + id: Int + access_token: String + ): SubCommentOutputDelete + + create_log( + path: String + username: String + email: String + status: String + api_access_time: Float + method: String + status_code: Int + ): Log + + delete_log( + id: Int + ): LogOutputDelete + + reset_logs: LogOutputReset + } +`; + +module.exports = mutations; diff --git a/server/orchestrators/graphqlOrchestrator/src/schema/queries/index.js b/server/orchestrators/graphqlOrchestrator/src/schema/queries/index.js new file mode 100644 index 0000000..a032381 --- /dev/null +++ b/server/orchestrators/graphqlOrchestrator/src/schema/queries/index.js @@ -0,0 +1,23 @@ +const queries = ` + type Query { + posts: [Post] + + search_posts( + title: String + sort: String + order: String + ): PostOutputSearch + + post_by_id( + id: Int + ): Post + + posts_by_user_id( + id: Int + ): [Post] + + logs: [Log] + } +`; + +module.exports = queries; diff --git a/server/services/users/package-lock.json b/server/services/users/package-lock.json index 3d3df05..eaace32 100644 --- a/server/services/users/package-lock.json +++ b/server/services/users/package-lock.json @@ -120,6 +120,14 @@ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" }, + "axios": { + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.0.tgz", + "integrity": "sha512-fmkJBknJKoZwem3/IKSSLpkdNXZeBu5Q7GA/aRsr2btgrptmSCxi2oFjZHqGdK9DoTil9PIHlPIZw2EcRJXRvw==", + "requires": { + "follow-redirects": "^1.10.0" + } + }, "balanced-match": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", @@ -616,6 +624,11 @@ "to-regex-range": "^5.0.1" } }, + "follow-redirects": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.13.0.tgz", + "integrity": "sha512-aq6gF1BEKje4a9i9+5jimNFIpq4Q1WiwBToeRK5NvZBd/TRsmW8BsJfOEGkr76TbOyPVD3OVDN910EcUNtRYEA==" + }, "form-data": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz", diff --git a/server/services/users/package.json b/server/services/users/package.json index 60a2ee2..fd191f0 100644 --- a/server/services/users/package.json +++ b/server/services/users/package.json @@ -4,6 +4,7 @@ "description": "", "main": "index.js", "dependencies": { + "axios": "^0.21.0", "bcryptjs": "^2.4.3", "dotenv": "^8.2.0", "jsonwebtoken": "^8.5.1", diff --git a/server/services/users/src/controllers/CommentController.js b/server/services/users/src/controllers/CommentController.js index 136f691..2bdeedd 100644 --- a/server/services/users/src/controllers/CommentController.js +++ b/server/services/users/src/controllers/CommentController.js @@ -1,9 +1,11 @@ const { Comment, Post, User } = require('../models'); const errorHandler = require('../helpers/errorHandler'); const sendEmail = require('../helpers/mailgun'); +const log = require('../helpers/logger'); class CommentController { static async create(ctx) { + const start_time = Date.now(); const UserId = ctx.user.id; if (ctx.request.body.content === undefined) { ctx.request.body.content = null; @@ -20,6 +22,13 @@ class CommentController { const comment = await Comment.create({ content, PostId, UserId }); ctx.response.status = 201; ctx.response.body = comment; + log( + `${ctx.request.host}${ctx.request.url}`, + ctx.request.header.access_token, + start_time, + ctx.request, + ctx.response, + ); // Send email with Mailgun API : const email_data = { @@ -36,10 +45,18 @@ Someone just commented "${comment.content}" on your post entitled "${post.title} const { status, errors } = errorHandler(err); ctx.response.status = status; ctx.response.body = errors; + log( + `${ctx.request.host}${ctx.request.url}`, + ctx.request.header.access_token, + start_time, + ctx.request, + ctx.response, + ); } } static async delete(ctx) { + const start_time = Date.now(); const id = +ctx.request.params.id; try { const deleted_comment = await Comment.findByPk(id); @@ -49,10 +66,24 @@ Someone just commented "${comment.content}" on your post entitled "${post.title} message: 'Delete Comment Success', deleted_comment, }; + log( + `${ctx.request.host}${ctx.request.url}`, + ctx.request.header.access_token, + start_time, + ctx.request, + ctx.response, + ); } catch(err) { const { status, errors } = errorHandler(err); ctx.response.status = status; ctx.response.body = errors; + log( + `${ctx.request.host}${ctx.request.url}`, + ctx.request.header.access_token, + start_time, + ctx.request, + ctx.response, + ); } } } diff --git a/server/services/users/src/controllers/LikeController.js b/server/services/users/src/controllers/LikeController.js index 78107dc..7639749 100644 --- a/server/services/users/src/controllers/LikeController.js +++ b/server/services/users/src/controllers/LikeController.js @@ -1,9 +1,11 @@ const { Like, Post, User } = require('../models'); const errorHandler = require('../helpers/errorHandler'); const sendEmail = require('../helpers/mailgun'); +const log = require('../helpers/logger'); class LikeController { static async create(ctx) { + const start_time = Date.now(); const UserId = ctx.user.id; if (ctx.request.body.PostId === undefined) { ctx.request.body.PostId = null; @@ -21,6 +23,13 @@ class LikeController { const like = await Like.create({ PostId, UserId }); ctx.response.status = 201; ctx.response.body = like; + log( + `${ctx.request.host}${ctx.request.url}`, + ctx.request.header.access_token, + start_time, + ctx.request, + ctx.response, + ); // Send email with Mailgun API : const email_data = { @@ -40,10 +49,18 @@ Someone just liked your post entitled "${post.title}".`, const { status, errors } = errorHandler(err); ctx.response.status = status; ctx.response.body = errors; + log( + `${ctx.request.host}${ctx.request.url}`, + ctx.request.header.access_token, + start_time, + ctx.request, + ctx.response, + ); } } static async delete(ctx) { + const start_time = Date.now(); const id = +ctx.request.params.id; try { const deleted_like = await Like.findByPk(id); @@ -53,10 +70,24 @@ Someone just liked your post entitled "${post.title}".`, message: 'Delete Like Success', deleted_like, }; + log( + `${ctx.request.host}${ctx.request.url}`, + ctx.request.header.access_token, + start_time, + ctx.request, + ctx.response, + ); } catch(err) { const { status, errors } = errorHandler(err); ctx.response.status = status; ctx.response.body = errors; + log( + `${ctx.request.host}${ctx.request.url}`, + ctx.request.header.access_token, + start_time, + ctx.request, + ctx.response, + ); } } } diff --git a/server/services/users/src/controllers/PostController.js b/server/services/users/src/controllers/PostController.js index e03af2d..775eca5 100644 --- a/server/services/users/src/controllers/PostController.js +++ b/server/services/users/src/controllers/PostController.js @@ -2,9 +2,11 @@ const { Post, User, Like, Comment, SubComment } = require('../models'); const { Op } = require('sequelize'); const errorHandler = require('../helpers/errorHandler'); const filterPost = require('../helpers/filterPost'); +const log = require('../helpers/logger'); class PostController { static async create(ctx) { + const start_time = Date.now(); const UserId = ctx.user.id; if (ctx.request.body.title === undefined) { ctx.request.body.title = null; @@ -17,14 +19,29 @@ class PostController { const new_post = await Post.create({ title, content, UserId }); ctx.response.status = 201; ctx.response.body = new_post; + log( + `${ctx.request.host}${ctx.request.url}`, + ctx.request.header.access_token, + start_time, + ctx.request, + ctx.response, + ); } catch(err) { const { status, errors } = errorHandler(err); ctx.response.status = status; ctx.response.body = errors; + log( + `${ctx.request.host}${ctx.request.url}`, + ctx.request.header.access_token, + start_time, + ctx.request, + ctx.response, + ); } } static async read(ctx) { + const start_time = Date.now(); try { const all_posts = await Post.findAll({ include: [{ @@ -39,14 +56,29 @@ class PostController { }); ctx.response.status = 200; ctx.response.body = all_posts; + log( + `${ctx.request.host}${ctx.request.url}`, + ctx.request.header.access_token, + start_time, + ctx.request, + ctx.response, + ); } catch(err) { const { status, errors } = errorHandler(err); ctx.response.status = status; ctx.response.body = errors; + log( + `${ctx.request.host}${ctx.request.url}`, + ctx.request.header.access_token, + start_time, + ctx.request, + ctx.response, + ); } } static async findByPostId(ctx) { + const start_time = Date.now(); const id = +ctx.request.params.id; try { const posts = await Post.findByPk(id, { @@ -62,14 +94,29 @@ class PostController { }); ctx.response.status = 200; ctx.response.body = posts; + log( + `${ctx.request.host}${ctx.request.url}`, + ctx.request.header.access_token, + start_time, + ctx.request, + ctx.response, + ); } catch(err) { const { status, errors } = errorHandler(err); ctx.response.status = status; ctx.response.body = errors; + log( + `${ctx.request.host}${ctx.request.url}`, + ctx.request.header.access_token, + start_time, + ctx.request, + ctx.response, + ); } } static async findByUserId(ctx) { + const start_time = Date.now(); const id = +ctx.request.params.id; try { const posts = await Post.findAll({ @@ -86,14 +133,29 @@ class PostController { }); ctx.response.status = 200; ctx.response.body = posts; + log( + `${ctx.request.host}${ctx.request.url}`, + ctx.request.header.access_token, + start_time, + ctx.request, + ctx.response, + ); } catch(err) { const { status, errors } = errorHandler(err); ctx.response.status = status; ctx.response.body = errors; + log( + `${ctx.request.host}${ctx.request.url}`, + ctx.request.header.access_token, + start_time, + ctx.request, + ctx.response, + ); } } static async search(ctx) { + const start_time = Date.now(); const { title, sort, order } = ctx.request.query; const sort_array = sort.split(','); const sort_order = filterPost(sort_array, order); @@ -117,14 +179,29 @@ class PostController { }); ctx.response.status = 200; ctx.response.body = { count, data }; - } catch(err) { console.log(err); + log( + `${ctx.request.host}${ctx.request.url}`, + ctx.request.header.access_token, + start_time, + ctx.request, + ctx.response, + ); + } catch(err) { const { status, errors } = errorHandler(err); ctx.response.status = status; ctx.response.body = errors; + log( + `${ctx.request.host}${ctx.request.url}`, + ctx.request.header.access_token, + start_time, + ctx.request, + ctx.response, + ); } } static async update(ctx) { + const start_time = Date.now(); const id = +ctx.request.params.id; if (ctx.request.body.title === undefined) { ctx.request.body.title = null; @@ -140,14 +217,29 @@ class PostController { }); ctx.response.status = 200; ctx.response.body = updated_post[1][0]; + log( + `${ctx.request.host}${ctx.request.url}`, + ctx.request.header.access_token, + start_time, + ctx.request, + ctx.response, + ); } catch(err) { const { status, errors } = errorHandler(err); ctx.response.status = status; ctx.response.body = errors; + log( + `${ctx.request.host}${ctx.request.url}`, + ctx.request.header.access_token, + start_time, + ctx.request, + ctx.response, + ); } } static async delete(ctx) { + const start_time = Date.now(); const id = +ctx.request.params.id; try { const deleted_post = await Post.findByPk(id); @@ -157,10 +249,24 @@ class PostController { message: 'Delete Post Success', deleted_post, }; + log( + `${ctx.request.host}${ctx.request.url}`, + ctx.request.header.access_token, + start_time, + ctx.request, + ctx.response, + ); } catch(err) { const { status, errors } = errorHandler(err); ctx.response.status = status; ctx.response.body = errors; + log( + `${ctx.request.host}${ctx.request.url}`, + ctx.request.header.access_token, + start_time, + ctx.request, + ctx.response, + ); } } } diff --git a/server/services/users/src/controllers/SubCommentController.js b/server/services/users/src/controllers/SubCommentController.js index d94533e..7c36013 100644 --- a/server/services/users/src/controllers/SubCommentController.js +++ b/server/services/users/src/controllers/SubCommentController.js @@ -1,9 +1,11 @@ const { SubComment, Comment, User, Post } = require('../models'); const errorHandler = require('../helpers/errorHandler'); const sendEmail = require('../helpers/mailgun'); +const log = require('../helpers/logger'); class SubCommentController { static async create(ctx) { + const start_time = Date.now(); const UserId = ctx.user.id; if (ctx.request.body.content === undefined) { ctx.request.body.content = null; @@ -20,6 +22,13 @@ class SubCommentController { const sub_comment = await SubComment.create({ content, CommentId, UserId }); ctx.response.status = 201; ctx.response.body = sub_comment; + log( + `${ctx.request.host}${ctx.request.url}`, + ctx.request.header.access_token, + start_time, + ctx.request, + ctx.response, + ); // Send email with Mailgun API : const email_data = { @@ -36,10 +45,18 @@ Someone just commented "${sub_comment.content}" on your comment "${comment.conte const { status, errors } = errorHandler(err); ctx.response.status = status; ctx.response.body = errors; + log( + `${ctx.request.host}${ctx.request.url}`, + ctx.request.header.access_token, + start_time, + ctx.request, + ctx.response, + ); } } static async delete(ctx) { + const start_time = Date.now(); const id = +ctx.request.params.id; try { const deleted_sub_comment = await SubComment.findByPk(id); @@ -49,10 +66,24 @@ Someone just commented "${sub_comment.content}" on your comment "${comment.conte message: 'Delete Sub Comment Success', deleted_sub_comment, }; + log( + `${ctx.request.host}${ctx.request.url}`, + ctx.request.header.access_token, + start_time, + ctx.request, + ctx.response, + ); } catch(err) { const { status, errors } = errorHandler(err); ctx.response.status = status; ctx.response.body = errors; + log( + `${ctx.request.host}${ctx.request.url}`, + ctx.request.header.access_token, + start_time, + ctx.request, + ctx.response, + ); } } } diff --git a/server/services/users/src/controllers/UserController.js b/server/services/users/src/controllers/UserController.js index 9c28d6e..a51175d 100644 --- a/server/services/users/src/controllers/UserController.js +++ b/server/services/users/src/controllers/UserController.js @@ -3,18 +3,33 @@ const { compare_bcrypt_password } = require('../helpers/bcrypt'); const { generate_jwt_token, verify_jwt_token } = require('../helpers/jwt'); const errorHandler = require('../helpers/errorHandler'); const sendEmail = require('../helpers/mailgun'); +const log = require('../helpers/logger'); class UserController { static async register(ctx) { + const start_time = Date.now(); const { username, email, password } = ctx.request.body; try { const new_user = await User.create({ username, email, password }); - ctx.response.status = 201; - ctx.response.body = new_user; // Generate user verification token : const verification_token = generate_jwt_token(new_user); + // Response : + ctx.response.status = 201; + ctx.response.body = { + message: 'User Registration Success', + verification_token, + data: new_user, + }; + log( + `${ctx.request.host}${ctx.request.url}`, + ctx.request.header.access_token, + start_time, + ctx.request, + ctx.response, + ); + // Send email with Mailgun API : const url = `http://localhost:3001/users/verify?token=${verification_token}`; const email_data = { @@ -28,10 +43,18 @@ class UserController { const { status, errors } = errorHandler(err); ctx.response.status = status; ctx.response.body = errors; + log( + `${ctx.request.host}${ctx.request.url}`, + ctx.request.header.access_token, + start_time, + ctx.request, + ctx.response, + ); } } static async verify(ctx) { + const start_time = Date.now(); const { token } = ctx.request.query; try { const decoded_user_data = verify_jwt_token(token); @@ -42,7 +65,7 @@ class UserController { } }); - // Validate the token : + // Validate the verification token : if (!user) { throw new Error('The verification link is invalid.'); } else if (user.status === 'active') { @@ -53,16 +76,34 @@ class UserController { returning: true, }); ctx.response.status = 200; - ctx.response.body = { message: 'User Verification Success' }; + ctx.response.body = { + message: 'User Verification Success', + data: user_activated[1][0], + }; + log( + `${ctx.request.host}${ctx.request.url}`, + ctx.request.header.access_token, + start_time, + ctx.request, + ctx.response, + ); } } catch(err) { const { status, errors } = errorHandler(err); ctx.response.status = status; ctx.response.body = errors; + log( + `${ctx.request.host}${ctx.request.url}`, + ctx.request.header.access_token, + start_time, + ctx.request, + ctx.response, + ); } } static async login(ctx) { + const start_time = Date.now(); const { email, password } = ctx.request.body; try { const user = await User.findOne({ where: { email }}); @@ -79,13 +120,31 @@ class UserController { } else { const access_token = generate_jwt_token(user); ctx.response.status = 200; - ctx.response.body = { access_token, user_id: user.id }; + ctx.response.body = { + message: 'User Login Success', + access_token, + user_id: user.id, + }; + log( + `${ctx.request.host}${ctx.request.url}`, + ctx.request.header.access_token, + start_time, + ctx.request, + ctx.response, + ); } } } catch(err) { const { status, errors } = errorHandler(err); ctx.response.status = status; ctx.response.body = errors; + log( + `${ctx.request.host}${ctx.request.url}`, + ctx.request.header.access_token, + start_time, + ctx.request, + ctx.response, + ); } } } diff --git a/server/services/users/src/helpers/logger.js b/server/services/users/src/helpers/logger.js new file mode 100644 index 0000000..49af530 --- /dev/null +++ b/server/services/users/src/helpers/logger.js @@ -0,0 +1,32 @@ +const { User } = require('../models'); +const { verify_jwt_token } = require('./jwt'); +const axios = require('axios'); + +async function log(path, access_token, request_start_time, request_object, response_object) { + let user_detail; + if (!access_token) { + user_detail = { + name: 'anonymous', + status: 'unknown', + }; + } else { + const decoded_user_data = verify_jwt_token(access_token); + const { id, username, email, status } = decoded_user_data; + user_detail = { id, username, email, status }; + } + + try { + await axios.post('http://localhost:3002/logs', { + path, + user_detail, + api_access_time: Date.now() - request_start_time, + request_object, + response_object, + }); + console.log('logged successfully'); + } catch(err) { + console.log(err); + } +} + +module.exports = log; diff --git a/server/services/users/src/middlewares/authentication.js b/server/services/users/src/middlewares/authentication.js index e3165a8..f3c25e7 100644 --- a/server/services/users/src/middlewares/authentication.js +++ b/server/services/users/src/middlewares/authentication.js @@ -1,8 +1,10 @@ const { User } = require('../models'); const { verify_jwt_token } = require('../helpers/jwt'); const errorHandler = require('../helpers/errorHandler'); +const log = require('../helpers/logger'); async function authentication(ctx, next) { + const start_time = Date.now(); const { access_token } = ctx.request.header; try { const decoded_user_data = verify_jwt_token(access_token); @@ -19,6 +21,13 @@ async function authentication(ctx, next) { const { status, errors } = errorHandler(err); ctx.response.status = status; ctx.response.body = errors; + log( + `${ctx.request.host}${ctx.request.url}`, + ctx.request.header.access_token, + start_time, + ctx.request, + ctx.response, + ); } } diff --git a/server/services/users/src/middlewares/authorization.js b/server/services/users/src/middlewares/authorization.js index 1d99af4..105fd09 100644 --- a/server/services/users/src/middlewares/authorization.js +++ b/server/services/users/src/middlewares/authorization.js @@ -1,9 +1,11 @@ const { User, Post, Like, Comment, SubComment } = require('../models'); const { verify_jwt_token } = require('../helpers/jwt'); const errorHandler = require('../helpers/errorHandler'); +const log = require('../helpers/logger'); // authorization check before updating or deletinga post : async function authorization_post(ctx, next) { + const start_time = Date.now(); const id = +ctx.request.params.id; try { const post = await Post.findByPk(id); @@ -18,11 +20,19 @@ async function authorization_post(ctx, next) { const { status, errors } = errorHandler(err); ctx.response.status = status; ctx.response.body = errors; + log( + `${ctx.request.host}${ctx.request.url}`, + ctx.request.header.access_token, + start_time, + ctx.request, + ctx.response, + ); } } // authorization check before deleting a like : async function authorization_like(ctx, next) { + const start_time = Date.now(); const id = +ctx.request.params.id; try { const like = await Like.findByPk(id); @@ -37,11 +47,19 @@ async function authorization_like(ctx, next) { const { status, errors } = errorHandler(err); ctx.response.status = status; ctx.response.body = errors; + log( + `${ctx.request.host}${ctx.request.url}`, + ctx.request.header.access_token, + start_time, + ctx.request, + ctx.response, + ); } } // authorization check before deleting a comment : async function authorization_comment(ctx, next) { + const start_time = Date.now(); const id = +ctx.request.params.id; try { const comment = await Comment.findByPk(id); @@ -56,12 +74,20 @@ async function authorization_comment(ctx, next) { const { status, errors } = errorHandler(err); ctx.response.status = status; ctx.response.body = errors; + log( + `${ctx.request.host}${ctx.request.url}`, + ctx.request.header.access_token, + start_time, + ctx.request, + ctx.response, + ); } } // authorization check before deleting a sub comment : async function authorization_sub_comment(ctx, next) { - const id = +ctx.request.params.id; console.log({id}); + const start_time = Date.now(); + const id = +ctx.request.params.id; try { const sub_comment = await SubComment.findByPk(id); if (sub_comment && sub_comment.UserId === ctx.user.id) { @@ -75,6 +101,13 @@ async function authorization_sub_comment(ctx, next) { const { status, errors } = errorHandler(err); ctx.response.status = status; ctx.response.body = errors; + log( + `${ctx.request.host}${ctx.request.url}`, + ctx.request.header.access_token, + start_time, + ctx.request, + ctx.response, + ); } } diff --git a/server/services/users/users_api.md b/users_api.md similarity index 91% rename from server/services/users/users_api.md rename to users_api.md index 13ff8ed..83216fe 100644 --- a/server/services/users/users_api.md +++ b/users_api.md @@ -6,7 +6,8 @@ This document explains the Users services. The Users service has : * RESTful endpoints for user registration, verification, and login. -* RESTful endpoints for CRUD operations of blog posts, likes, comments, and sub comments. +* RESTful endpoints for CRUD, search, find-by-id, and find-by-user-id operations of blog posts. +* RESTful endpoints for Create and Delete operations of likes, comments, and sub comments. * JSON formatted response.   @@ -57,23 +58,25 @@ _Request Body_ _Response (201 - Created)_ ``` { - "id": , - "username": "", - "email": "", - "password": "", - "status": "registered", - "createdAt": "", - "updatedAt": "" + "message": "User Registration Success", + "verification_token": "", + "data": { + "id": , + "username": "", + "email": "", + "password": "", + "status": "registered", + "createdAt": "", + "updatedAt": "" + } } ``` _Response (400 - Bad Request)_ ``` [ - "", - "", - ..., - "" + "", + ... ] ``` @@ -84,7 +87,7 @@ _Response (500 - Internal Server Error)_ ] ``` --- -### GET /users/verify?token= +### GET /users/verify?token= > Verify user @@ -101,7 +104,16 @@ not needed _Response (200 - OK)_ ``` { - "message": "User Verification Success" + "message": "User Verification Success", + "data": { + "id": , + "username": <"user's name">, + "email": "", + "password": "", + "status": "active", + "createdAt": "", + "updatedAt": "" + } } ``` @@ -143,6 +155,7 @@ _Request Body_ _Response (200 - OK)_ ``` { + "message": "User Login Success", "access_token": "", "user_id": "" } @@ -200,10 +213,8 @@ _Response (201 - Created)_ _Response (400 - Bad Request)_ ``` [ - "", - "", - ..., - "" + "", + ... ] ``` @@ -294,10 +305,8 @@ _Response (200 - OK)_ _Response (400 - Bad Request)_ ``` [ - "", - "", - ..., - "" + "", + ... ] ``` @@ -405,10 +414,8 @@ _Response (200 - OK)_ _Response (400 - Bad Request)_ ``` [ - "", - "", - ..., - "" + "", + ... ] ``` @@ -496,10 +503,8 @@ _Response (200 - OK)_ _Response (400 - Bad Request)_ ``` [ - "", - "", - ..., - "" + "", + ... ] ``` @@ -590,10 +595,8 @@ _Response (200 - OK)_ _Response (400 - Bad Request)_ ``` [ - "", - "", - ..., - "" + "", + ... ] ``` @@ -645,10 +648,8 @@ _Response (200 - OK)_ _Response (400 - Bad Request)_ ``` [ - "", - "", - ..., - "" + "", + ... ] ``` @@ -707,10 +708,8 @@ _Response (200 - OK)_ _Response (400 - Bad Request)_ ``` [ - "", - "", - ..., - "" + "", + ... ] ``` @@ -769,10 +768,8 @@ _Response (201 - Created)_ _Response (400 - Bad Request)_ ``` [ - "", - "", - ..., - "" + "", + ... ] ``` @@ -831,10 +828,8 @@ _Response (200 - OK)_ _Response (400 - Bad Request)_ ``` [ - "", - "", - ..., - "" + "", + ... ] ``` @@ -893,10 +888,8 @@ _Response (201 - Created)_ _Response (400 - Bad Request)_ ``` [ - "", - "", - ..., - "" + "", + ... ] ``` @@ -955,10 +948,8 @@ _Response (200 - OK)_ _Response (400 - Bad Request)_ ``` [ - "", - "", - ..., - "" + "", + ... ] ``` @@ -1015,10 +1006,8 @@ _Response (201 - Created)_ _Response (400 - Bad Request)_ ``` [ - "", - "", - ..., - "" + "", + ... ] ``` @@ -1076,10 +1065,8 @@ _Response (200 - OK)_ _Response (400 - Bad Request)_ ``` [ - "", - "", - ..., - "" + "", + ... ] ``` From fdd846d7a2bad6a8f91896e2ec1716f14fb148b2 Mon Sep 17 00:00:00 2001 From: Alfian Tirta Date: Mon, 30 Nov 2020 13:31:28 +0700 Subject: [PATCH 23/37] fixed redis --- .../src/resolvers/mutations/index.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/server/orchestrators/graphqlOrchestrator/src/resolvers/mutations/index.js b/server/orchestrators/graphqlOrchestrator/src/resolvers/mutations/index.js index 575b035..e4890c5 100644 --- a/server/orchestrators/graphqlOrchestrator/src/resolvers/mutations/index.js +++ b/server/orchestrators/graphqlOrchestrator/src/resolvers/mutations/index.js @@ -14,47 +14,59 @@ async function login_user(_, user, { dataSources }) { // handle post features : async function create_post(_, { title, content, access_token }, { dataSources }) { + await redis.del('posts'); return dataSources.usersAPI.createPost({ title, content }, access_token); } async function update_post(_, { id, title, content, access_token }, { dataSources }) { + await redis.del('posts'); return dataSources.usersAPI.updatePost(id, { title, content }, access_token); } async function delete_post(_, { id, access_token }, { dataSources }) { + await redis.del('posts'); return dataSources.usersAPI.deletePost(id, access_token); } // handle like-unlike features : async function create_like(_, { PostId, access_token }, { dataSources }) { + await redis.del('posts'); return dataSources.usersAPI.createLike({ PostId }, access_token); } async function delete_like(_, { id, access_token }, { dataSources }) { + await redis.del('posts'); return dataSources.usersAPI.deleteLike(id, access_token); } // handle comment features : async function create_comment(_, { content, PostId, access_token }, { dataSources }) { + await redis.del('posts'); return dataSources.usersAPI.createComment({ content, PostId }, access_token); } async function delete_comment(_, { id, access_token }, { dataSources }) { + await redis.del('posts'); return dataSources.usersAPI.deleteComment(id, access_token); } // handle sub comment features : async function create_sub_comment(_, { content, CommentId, access_token }, { dataSources }) { + await redis.del('posts'); return dataSources.usersAPI.createSubComment({ content, CommentId }, access_token); } async function delete_sub_comment(_, { id, access_token }, { dataSources }) { + await redis.del('posts'); return dataSources.usersAPI.deleteSubComment(id, access_token); } // handle log features : async function create_log(_, log, { dataSources }) { + await redis.del('logs'); return dataSources.logsAPI.createLog(log); } async function delete_log(_, { id }, { dataSources }) { + await redis.del('logs'); return dataSources.logsAPI.deleteLog(id); } async function reset_logs(_, __, { dataSources }) { + await redis.del('logs'); return dataSources.logsAPI.resetLogs(); } From 0a77d3a99779838eb04bb4eb1c1dbdde7f999bbd Mon Sep 17 00:00:00 2001 From: Alfian Tirta Date: Mon, 30 Nov 2020 13:40:44 +0700 Subject: [PATCH 24/37] fixed typo: "seconds" to "milliseconds" in logs_api.md --- logs_api.md | 2 +- server/services/logs/logs_api.md | 200 +++++++++++++++++++++++++++++++ 2 files changed, 201 insertions(+), 1 deletion(-) create mode 100644 server/services/logs/logs_api.md diff --git a/logs_api.md b/logs_api.md index 4120387..92925f9 100644 --- a/logs_api.md +++ b/logs_api.md @@ -138,7 +138,7 @@ _Response (200 - OK)_ "_id": "" "path": "", "user_detail": {user detail object}, - "api_access_time": , + "api_access_time": , "request_object": {api request object}, "response_object": {api response object}, "createdAt": "", diff --git a/server/services/logs/logs_api.md b/server/services/logs/logs_api.md new file mode 100644 index 0000000..4120387 --- /dev/null +++ b/server/services/logs/logs_api.md @@ -0,0 +1,200 @@ +# Blog App - Logs Service API +Blog App is an application to manage your blog. It has two services : Users & Logs. + +This document explains the Logs services. + +The Logs service has : + +* RESTful endpoints for create, read, delete, and reset operations of blog access logs. +* JSON formatted response. + +  + +## Endpoints +``` + - POST /logs + - GET /logs + - DELETE /posts/:id + - DELETE /logs +``` + +## RESTful endpoints +### POST /logs + +> Create blog access log + +_Request Header_ +``` +not needed +``` + +_Request Body_ +``` +{ + "path": "", + "user_detail": {user detail object}, + "api_access_time": , + "request_object": {api request object}, + "response_object": {api response object} +} +``` + +_Response (201 - Created)_ +``` +{ + "_id": "" + "path": "", + "user_detail": {user detail object}, + "api_access_time": , + "request_object": {api request object}, + "response_object": {api response object}, + "createdAt": "", + "updatedAt": "", + "__v": +} +``` + +_Response (400 - Bad Request)_ +``` +[ + "", + ... +] +``` + +_Response (500 - Internal Server Error)_ +``` +[ + "Internal Server Error" +] +``` +--- +### GET /logs + +> Get all blog access logs + +_Request Header_ +``` +not needed +``` + +_Request Body_ +``` +not needed +``` + +_Response (200 - OK)_ +``` +[ + { + "_id": "" + "path": "", + "user_detail": {user detail object}, + "api_access_time": , + "request_object": {api request object}, + "response_object": {api response object}, + "createdAt": "", + "updatedAt": "", + "__v": + }, + ... +] +``` + +_Response (400 - Bad Request)_ +``` +[ + "", + ... +] +``` + +_Response (500 - Internal Server Error)_ +``` +[ + "Internal Server Error" +] +``` +--- +### DELETE /logs/:id + +> Delete a blog access log by its id + +_Request Header_ +``` +not needed +``` + +_Request Body_ +``` +not needed +``` + +_Response (200 - OK)_ +``` +{ + "message": "Delete Log Success", + "deleted_log": { + "_id": "" + "path": "", + "user_detail": {user detail object}, + "api_access_time": , + "request_object": {api request object}, + "response_object": {api response object}, + "createdAt": "", + "updatedAt": "", + "__v": + } +} +``` + +_Response (400 - Bad Request)_ +``` +[ + "", + ... +] +``` + +_Response (500 - Internal Server Error)_ +``` +[ + "Internal Server Error" +] +``` +--- +### DELETE /logs + +> Delete all (reset) blog access logs by its id + +_Request Header_ +``` +not needed +``` + +_Request Body_ +``` +not needed +``` + +_Response (200 - OK)_ +``` +{ + "message": "Reset Logs Success" +} +``` + +_Response (400 - Bad Request)_ +``` +[ + "", + ... +] +``` + +_Response (500 - Internal Server Error)_ +``` +[ + "Internal Server Error" +] +``` \ No newline at end of file From f59b358d54406a97e22468fbce376c83364710bf Mon Sep 17 00:00:00 2001 From: Alfian Tirta Date: Tue, 1 Dec 2020 10:05:26 +0700 Subject: [PATCH 25/37] modified --- .../src/dataSources/usersAPI.js | 32 +++++++++++++----- .../src/resolvers/mutations/index.js | 3 -- .../src/resolvers/queries/index.js | 25 +++++--------- .../src/schema/queries/index.js | 7 +++- server/services/users/src/config/config.js | 12 +++---- .../src/controllers/CommentController.js | 4 +++ .../users/src/controllers/LikeController.js | 4 +++ .../users/src/controllers/PostController.js | 14 ++++++++ .../src/controllers/SubCommentController.js | 4 +++ .../users/src/controllers/UserController.js | 8 ++++- server/services/users/src/helpers/logger.js | 33 ++++++++++++++----- .../users/src/middlewares/authentication.js | 1 + .../users/src/middlewares/authorization.js | 4 +++ users_api.md | 20 ++++++++--- 14 files changed, 123 insertions(+), 48 deletions(-) diff --git a/server/orchestrators/graphqlOrchestrator/src/dataSources/usersAPI.js b/server/orchestrators/graphqlOrchestrator/src/dataSources/usersAPI.js index beacaf4..6dbd855 100644 --- a/server/orchestrators/graphqlOrchestrator/src/dataSources/usersAPI.js +++ b/server/orchestrators/graphqlOrchestrator/src/dataSources/usersAPI.js @@ -25,17 +25,33 @@ class UsersAPI extends RESTDataSource { }, }); } - async readPosts() { - return this.get('/posts'); + async readPosts(token) { + return this.get('/posts', null, { + headers: { + access_token: token, + }, + }); } - async searchPosts(title, sort, order) { - return this.get(`/posts/search?title=${title}&sort=${sort}&order=${order}`); + async searchPosts(title, sort, order, token) { + return this.get(`/posts/search?title=${title}&sort=${sort}&order=${order}`, null, { + headers: { + access_token: token, + }, + }); } - async findPostById(id) { - return this.get(`/posts/${id}`); + async findPostById(id, token) { + return this.get(`/posts/${id}`, null, { + headers: { + access_token: token, + }, + }); } - async findPostsByUserId(id) { - return this.get(`/posts/user/${id}`); + async findPostsByUserId(id, token) { + return this.get(`/posts/user/${id}`, null, { + headers: { + access_token: token, + }, + }); } async updatePost(id, post, token) { return this.put(`/posts/${id}`, post, { diff --git a/server/orchestrators/graphqlOrchestrator/src/resolvers/mutations/index.js b/server/orchestrators/graphqlOrchestrator/src/resolvers/mutations/index.js index e4890c5..341c527 100644 --- a/server/orchestrators/graphqlOrchestrator/src/resolvers/mutations/index.js +++ b/server/orchestrators/graphqlOrchestrator/src/resolvers/mutations/index.js @@ -58,15 +58,12 @@ async function delete_sub_comment(_, { id, access_token }, { dataSources }) { // handle log features : async function create_log(_, log, { dataSources }) { - await redis.del('logs'); return dataSources.logsAPI.createLog(log); } async function delete_log(_, { id }, { dataSources }) { - await redis.del('logs'); return dataSources.logsAPI.deleteLog(id); } async function reset_logs(_, __, { dataSources }) { - await redis.del('logs'); return dataSources.logsAPI.resetLogs(); } diff --git a/server/orchestrators/graphqlOrchestrator/src/resolvers/queries/index.js b/server/orchestrators/graphqlOrchestrator/src/resolvers/queries/index.js index 916aaa9..b339968 100644 --- a/server/orchestrators/graphqlOrchestrator/src/resolvers/queries/index.js +++ b/server/orchestrators/graphqlOrchestrator/src/resolvers/queries/index.js @@ -2,36 +2,29 @@ const Redis = require('ioredis'); const redis = new Redis(); // handle post features : -async function posts(_, __, { dataSources }) { +async function posts(_, { access_token }, { dataSources }) { const cached_posts = await redis.get('posts'); if (cached_posts) { return JSON.parse(cached_posts); } else { - const posts = await dataSources.usersAPI.readPosts(); + const posts = await dataSources.usersAPI.readPosts(access_token); await redis.set('posts', JSON.stringify(posts)); return posts; } } -async function search_posts(_, { title, sort, order }, { dataSources }) { - return dataSources.usersAPI.searchPosts(title, sort, order); +async function search_posts(_, { title, sort, order, access_token }, { dataSources }) { + return dataSources.usersAPI.searchPosts(title, sort, order, access_token); } -async function post_by_id(_, { id }, { dataSources }) { - return dataSources.usersAPI.findPostById(id); +async function post_by_id(_, { id, access_token }, { dataSources }) { + return dataSources.usersAPI.findPostById(id, access_token); } -async function posts_by_user_id(_, { id }, { dataSources }) { - return dataSources.usersAPI.findPostsByUserId(id); +async function posts_by_user_id(_, { id, access_token }, { dataSources }) { + return dataSources.usersAPI.findPostsByUserId(id, access_token); } // handle log feature : async function logs(_, __, { dataSources }) { - const cached_logs = await redis.get('logs'); - if (cached_logs) { - return JSON.parse(cached_logs); - } else { - const logs = await dataSources.logsAPI.readLogs(); - await redis.set('logs', JSON.stringify(logs)); - return logs; - } + return dataSources.logsAPI.readLogs(); } module.exports = { diff --git a/server/orchestrators/graphqlOrchestrator/src/schema/queries/index.js b/server/orchestrators/graphqlOrchestrator/src/schema/queries/index.js index a032381..3930783 100644 --- a/server/orchestrators/graphqlOrchestrator/src/schema/queries/index.js +++ b/server/orchestrators/graphqlOrchestrator/src/schema/queries/index.js @@ -1,19 +1,24 @@ const queries = ` type Query { - posts: [Post] + posts( + access_token: String + ): [Post] search_posts( title: String sort: String order: String + access_token: String ): PostOutputSearch post_by_id( id: Int + access_token: String ): Post posts_by_user_id( id: Int + access_token: String ): [Post] logs: [Log] diff --git a/server/services/users/src/config/config.js b/server/services/users/src/config/config.js index a139214..6dd088a 100644 --- a/server/services/users/src/config/config.js +++ b/server/services/users/src/config/config.js @@ -6,7 +6,7 @@ module.exports = { "password": process.env.DB_PASSWORD, "database": process.env.DB_NAME, "host": process.env.DB_HOST, - "dialect": "postgres", + "dialect": process.env.DB_DIALECT, }, "test": { "username": "root", @@ -16,10 +16,10 @@ module.exports = { "dialect": "mysql", }, "production": { - "username": "root", - "password": null, - "database": "database_production", - "host": "127.0.0.1", - "dialect": "mysql", + "username": process.env.DB_USERNAME, + "password": process.env.DB_PASSWORD, + "database": process.env.DB_NAME, + "host": process.env.DB_HOST, + "dialect": process.env.DB_DIALECT, } } diff --git a/server/services/users/src/controllers/CommentController.js b/server/services/users/src/controllers/CommentController.js index 2bdeedd..74958d9 100644 --- a/server/services/users/src/controllers/CommentController.js +++ b/server/services/users/src/controllers/CommentController.js @@ -24,6 +24,7 @@ class CommentController { ctx.response.body = comment; log( `${ctx.request.host}${ctx.request.url}`, + null, ctx.request.header.access_token, start_time, ctx.request, @@ -47,6 +48,7 @@ Someone just commented "${comment.content}" on your post entitled "${post.title} ctx.response.body = errors; log( `${ctx.request.host}${ctx.request.url}`, + null, ctx.request.header.access_token, start_time, ctx.request, @@ -68,6 +70,7 @@ Someone just commented "${comment.content}" on your post entitled "${post.title} }; log( `${ctx.request.host}${ctx.request.url}`, + null, ctx.request.header.access_token, start_time, ctx.request, @@ -79,6 +82,7 @@ Someone just commented "${comment.content}" on your post entitled "${post.title} ctx.response.body = errors; log( `${ctx.request.host}${ctx.request.url}`, + null, ctx.request.header.access_token, start_time, ctx.request, diff --git a/server/services/users/src/controllers/LikeController.js b/server/services/users/src/controllers/LikeController.js index 7639749..bc7afe3 100644 --- a/server/services/users/src/controllers/LikeController.js +++ b/server/services/users/src/controllers/LikeController.js @@ -25,6 +25,7 @@ class LikeController { ctx.response.body = like; log( `${ctx.request.host}${ctx.request.url}`, + null, ctx.request.header.access_token, start_time, ctx.request, @@ -51,6 +52,7 @@ Someone just liked your post entitled "${post.title}".`, ctx.response.body = errors; log( `${ctx.request.host}${ctx.request.url}`, + null, ctx.request.header.access_token, start_time, ctx.request, @@ -72,6 +74,7 @@ Someone just liked your post entitled "${post.title}".`, }; log( `${ctx.request.host}${ctx.request.url}`, + null, ctx.request.header.access_token, start_time, ctx.request, @@ -83,6 +86,7 @@ Someone just liked your post entitled "${post.title}".`, ctx.response.body = errors; log( `${ctx.request.host}${ctx.request.url}`, + null, ctx.request.header.access_token, start_time, ctx.request, diff --git a/server/services/users/src/controllers/PostController.js b/server/services/users/src/controllers/PostController.js index 775eca5..70c4afa 100644 --- a/server/services/users/src/controllers/PostController.js +++ b/server/services/users/src/controllers/PostController.js @@ -21,6 +21,7 @@ class PostController { ctx.response.body = new_post; log( `${ctx.request.host}${ctx.request.url}`, + null, ctx.request.header.access_token, start_time, ctx.request, @@ -32,6 +33,7 @@ class PostController { ctx.response.body = errors; log( `${ctx.request.host}${ctx.request.url}`, + null, ctx.request.header.access_token, start_time, ctx.request, @@ -58,6 +60,7 @@ class PostController { ctx.response.body = all_posts; log( `${ctx.request.host}${ctx.request.url}`, + null, ctx.request.header.access_token, start_time, ctx.request, @@ -69,6 +72,7 @@ class PostController { ctx.response.body = errors; log( `${ctx.request.host}${ctx.request.url}`, + null, ctx.request.header.access_token, start_time, ctx.request, @@ -96,6 +100,7 @@ class PostController { ctx.response.body = posts; log( `${ctx.request.host}${ctx.request.url}`, + null, ctx.request.header.access_token, start_time, ctx.request, @@ -107,6 +112,7 @@ class PostController { ctx.response.body = errors; log( `${ctx.request.host}${ctx.request.url}`, + null, ctx.request.header.access_token, start_time, ctx.request, @@ -135,6 +141,7 @@ class PostController { ctx.response.body = posts; log( `${ctx.request.host}${ctx.request.url}`, + null, ctx.request.header.access_token, start_time, ctx.request, @@ -146,6 +153,7 @@ class PostController { ctx.response.body = errors; log( `${ctx.request.host}${ctx.request.url}`, + null, ctx.request.header.access_token, start_time, ctx.request, @@ -181,6 +189,7 @@ class PostController { ctx.response.body = { count, data }; log( `${ctx.request.host}${ctx.request.url}`, + null, ctx.request.header.access_token, start_time, ctx.request, @@ -192,6 +201,7 @@ class PostController { ctx.response.body = errors; log( `${ctx.request.host}${ctx.request.url}`, + null, ctx.request.header.access_token, start_time, ctx.request, @@ -219,6 +229,7 @@ class PostController { ctx.response.body = updated_post[1][0]; log( `${ctx.request.host}${ctx.request.url}`, + null, ctx.request.header.access_token, start_time, ctx.request, @@ -230,6 +241,7 @@ class PostController { ctx.response.body = errors; log( `${ctx.request.host}${ctx.request.url}`, + null, ctx.request.header.access_token, start_time, ctx.request, @@ -251,6 +263,7 @@ class PostController { }; log( `${ctx.request.host}${ctx.request.url}`, + null, ctx.request.header.access_token, start_time, ctx.request, @@ -262,6 +275,7 @@ class PostController { ctx.response.body = errors; log( `${ctx.request.host}${ctx.request.url}`, + null, ctx.request.header.access_token, start_time, ctx.request, diff --git a/server/services/users/src/controllers/SubCommentController.js b/server/services/users/src/controllers/SubCommentController.js index 7c36013..a5f1062 100644 --- a/server/services/users/src/controllers/SubCommentController.js +++ b/server/services/users/src/controllers/SubCommentController.js @@ -24,6 +24,7 @@ class SubCommentController { ctx.response.body = sub_comment; log( `${ctx.request.host}${ctx.request.url}`, + null, ctx.request.header.access_token, start_time, ctx.request, @@ -47,6 +48,7 @@ Someone just commented "${sub_comment.content}" on your comment "${comment.conte ctx.response.body = errors; log( `${ctx.request.host}${ctx.request.url}`, + null, ctx.request.header.access_token, start_time, ctx.request, @@ -68,6 +70,7 @@ Someone just commented "${sub_comment.content}" on your comment "${comment.conte }; log( `${ctx.request.host}${ctx.request.url}`, + null, ctx.request.header.access_token, start_time, ctx.request, @@ -79,6 +82,7 @@ Someone just commented "${sub_comment.content}" on your comment "${comment.conte ctx.response.body = errors; log( `${ctx.request.host}${ctx.request.url}`, + null, ctx.request.header.access_token, start_time, ctx.request, diff --git a/server/services/users/src/controllers/UserController.js b/server/services/users/src/controllers/UserController.js index a51175d..4b80d4c 100644 --- a/server/services/users/src/controllers/UserController.js +++ b/server/services/users/src/controllers/UserController.js @@ -24,6 +24,7 @@ class UserController { }; log( `${ctx.request.host}${ctx.request.url}`, + { username, email }, ctx.request.header.access_token, start_time, ctx.request, @@ -31,7 +32,7 @@ class UserController { ); // Send email with Mailgun API : - const url = `http://localhost:3001/users/verify?token=${verification_token}`; + const url = `${process.env.BASE_URL}/users/verify?token=${verification_token}`; const email_data = { from: `Blog App Team `, to: `${new_user.email}`, @@ -45,6 +46,7 @@ class UserController { ctx.response.body = errors; log( `${ctx.request.host}${ctx.request.url}`, + null, ctx.request.header.access_token, start_time, ctx.request, @@ -82,6 +84,7 @@ class UserController { }; log( `${ctx.request.host}${ctx.request.url}`, + { username: user.username, email: user.email }, ctx.request.header.access_token, start_time, ctx.request, @@ -94,6 +97,7 @@ class UserController { ctx.response.body = errors; log( `${ctx.request.host}${ctx.request.url}`, + { username: user.username, email: user.email }, ctx.request.header.access_token, start_time, ctx.request, @@ -127,6 +131,7 @@ class UserController { }; log( `${ctx.request.host}${ctx.request.url}`, + { username: user.username, email: user.email }, ctx.request.header.access_token, start_time, ctx.request, @@ -140,6 +145,7 @@ class UserController { ctx.response.body = errors; log( `${ctx.request.host}${ctx.request.url}`, + { username: user.username, email: user.email }, ctx.request.header.access_token, start_time, ctx.request, diff --git a/server/services/users/src/helpers/logger.js b/server/services/users/src/helpers/logger.js index 49af530..6e49a88 100644 --- a/server/services/users/src/helpers/logger.js +++ b/server/services/users/src/helpers/logger.js @@ -2,24 +2,39 @@ const { User } = require('../models'); const { verify_jwt_token } = require('./jwt'); const axios = require('axios'); -async function log(path, access_token, request_start_time, request_object, response_object) { +async function log(path, user_object, access_token, request_start_time, request_object, response_object) { + const api_access_time = Date.now() - request_start_time; let user_detail; - if (!access_token) { + + if (access_token === 'null' || access_token === '') { + access_token = null; + } + + if (user_object) { + const user = await User.findOne({ where: user_object }); + user_detail = user; + } else if (access_token) { + const decoded_user_data = verify_jwt_token(access_token); console.log('hasilnya gimana guyssss =====', decoded_user_data); + const { id, username, email, status } = decoded_user_data; + const user = await User.findOne({ where: { id, username, email, status }}); + user_detail = user; + } else { user_detail = { - name: 'anonymous', - status: 'unknown', + id: null, + username: null, + email: null, + password: null, + status: null, + createdAt: null, + updatedAt: null, }; - } else { - const decoded_user_data = verify_jwt_token(access_token); - const { id, username, email, status } = decoded_user_data; - user_detail = { id, username, email, status }; } try { await axios.post('http://localhost:3002/logs', { path, user_detail, - api_access_time: Date.now() - request_start_time, + api_access_time, request_object, response_object, }); diff --git a/server/services/users/src/middlewares/authentication.js b/server/services/users/src/middlewares/authentication.js index f3c25e7..8e9649f 100644 --- a/server/services/users/src/middlewares/authentication.js +++ b/server/services/users/src/middlewares/authentication.js @@ -23,6 +23,7 @@ async function authentication(ctx, next) { ctx.response.body = errors; log( `${ctx.request.host}${ctx.request.url}`, + null, ctx.request.header.access_token, start_time, ctx.request, diff --git a/server/services/users/src/middlewares/authorization.js b/server/services/users/src/middlewares/authorization.js index 105fd09..b5774f4 100644 --- a/server/services/users/src/middlewares/authorization.js +++ b/server/services/users/src/middlewares/authorization.js @@ -22,6 +22,7 @@ async function authorization_post(ctx, next) { ctx.response.body = errors; log( `${ctx.request.host}${ctx.request.url}`, + null, ctx.request.header.access_token, start_time, ctx.request, @@ -49,6 +50,7 @@ async function authorization_like(ctx, next) { ctx.response.body = errors; log( `${ctx.request.host}${ctx.request.url}`, + null, ctx.request.header.access_token, start_time, ctx.request, @@ -76,6 +78,7 @@ async function authorization_comment(ctx, next) { ctx.response.body = errors; log( `${ctx.request.host}${ctx.request.url}`, + null, ctx.request.header.access_token, start_time, ctx.request, @@ -103,6 +106,7 @@ async function authorization_sub_comment(ctx, next) { ctx.response.body = errors; log( `${ctx.request.host}${ctx.request.url}`, + null, ctx.request.header.access_token, start_time, ctx.request, diff --git a/users_api.md b/users_api.md index 83216fe..912dc63 100644 --- a/users_api.md +++ b/users_api.md @@ -238,7 +238,10 @@ _Response (500 - Internal Server Error)_ _Request Header_ ``` -not needed +{ + "access_token": "" +} +*tidak wajib, hanya untuk keperluan logging data user ``` _Request Body_ @@ -344,7 +347,10 @@ _Response (500 - Internal Server Error)_ _Request Header_ ``` -not needed +{ + "access_token": "" +} +*tidak wajib, hanya untuk keperluan logging data user ``` _Request Body_ @@ -439,7 +445,10 @@ _Response (500 - Internal Server Error)_ _Request Header_ ``` -not needed +{ + "access_token": "" +} +*tidak wajib, hanya untuk keperluan logging data user ``` _Request Body_ @@ -528,7 +537,10 @@ _Response (500 - Internal Server Error)_ _Request Header_ ``` -not needed +{ + "access_token": "" +} +*tidak wajib, hanya untuk keperluan logging data user ``` _Request Body_ From db555fa7fff51e49e6ccfbfb107cb38ec769cd63 Mon Sep 17 00:00:00 2001 From: Alfian Tirta Date: Tue, 1 Dec 2020 12:52:18 +0700 Subject: [PATCH 26/37] fixed logs API bugs in graphqlOrchestrator --- .../src/dataSources/logsAPI.js | 5 +---- .../graphqlOrchestrator/src/index.js | 2 ++ .../src/resolvers/index.js | 2 -- .../src/resolvers/mutations/index.js | 4 ---- .../src/schema/models/index.js | 20 ++++++++++++++++++- .../src/schema/mutations/index.js | 12 +---------- server/services/logs/package.json | 2 +- server/services/users/package.json | 5 +---- 8 files changed, 25 insertions(+), 27 deletions(-) diff --git a/server/orchestrators/graphqlOrchestrator/src/dataSources/logsAPI.js b/server/orchestrators/graphqlOrchestrator/src/dataSources/logsAPI.js index ceb28f2..63656bd 100644 --- a/server/orchestrators/graphqlOrchestrator/src/dataSources/logsAPI.js +++ b/server/orchestrators/graphqlOrchestrator/src/dataSources/logsAPI.js @@ -5,10 +5,7 @@ class LogsAPI extends RESTDataSource { super(); this.baseURL = 'http://localhost:3002'; } - - async createLog(log) { - return this.post('/logs', log); - } + async readLogs() { return this.get('/logs'); } diff --git a/server/orchestrators/graphqlOrchestrator/src/index.js b/server/orchestrators/graphqlOrchestrator/src/index.js index 8129468..000b7c2 100644 --- a/server/orchestrators/graphqlOrchestrator/src/index.js +++ b/server/orchestrators/graphqlOrchestrator/src/index.js @@ -4,6 +4,7 @@ const { ApolloServer } = require('apollo-server'); const schema = require('./schema'); const resolvers = require('./resolvers'); const UsersAPI = require('./dataSources/usersAPI'); +const LogsAPI = require('./dataSources/logsAPI'); const server = new ApolloServer({ typeDefs: schema, @@ -11,6 +12,7 @@ const server = new ApolloServer({ dataSources() { return { usersAPI: new UsersAPI(), + logsAPI: new LogsAPI(), }; }, }); diff --git a/server/orchestrators/graphqlOrchestrator/src/resolvers/index.js b/server/orchestrators/graphqlOrchestrator/src/resolvers/index.js index 81a9f86..516f6e0 100644 --- a/server/orchestrators/graphqlOrchestrator/src/resolvers/index.js +++ b/server/orchestrators/graphqlOrchestrator/src/resolvers/index.js @@ -19,7 +19,6 @@ const { delete_comment, create_sub_comment, delete_sub_comment, - create_log, delete_log, reset_logs, } = require('./mutations'); @@ -45,7 +44,6 @@ const resolvers = { delete_comment, create_sub_comment, delete_sub_comment, - create_log, delete_log, reset_logs, }, diff --git a/server/orchestrators/graphqlOrchestrator/src/resolvers/mutations/index.js b/server/orchestrators/graphqlOrchestrator/src/resolvers/mutations/index.js index 341c527..dd98b2f 100644 --- a/server/orchestrators/graphqlOrchestrator/src/resolvers/mutations/index.js +++ b/server/orchestrators/graphqlOrchestrator/src/resolvers/mutations/index.js @@ -57,9 +57,6 @@ async function delete_sub_comment(_, { id, access_token }, { dataSources }) { } // handle log features : -async function create_log(_, log, { dataSources }) { - return dataSources.logsAPI.createLog(log); -} async function delete_log(_, { id }, { dataSources }) { return dataSources.logsAPI.deleteLog(id); } @@ -80,7 +77,6 @@ module.exports = { delete_comment, create_sub_comment, delete_sub_comment, - create_log, delete_log, reset_logs, }; diff --git a/server/orchestrators/graphqlOrchestrator/src/schema/models/index.js b/server/orchestrators/graphqlOrchestrator/src/schema/models/index.js index 024c80f..fff14a5 100644 --- a/server/orchestrators/graphqlOrchestrator/src/schema/models/index.js +++ b/server/orchestrators/graphqlOrchestrator/src/schema/models/index.js @@ -102,17 +102,35 @@ const models = ` } type UserDetail { + id: Int username: String email: String + password: String status: String + createdAt: String + updatedAt: String } type RequestObject { method: String + url: String + header: RequestHeader + } + + type RequestHeader { + host: String + accept: String + connection: String } type ResponseObject { - status_code: Int + status: String + message: String + header: ResponseHeader + } + + type ResponseHeader { + vary: String } type LogOutputDelete { diff --git a/server/orchestrators/graphqlOrchestrator/src/schema/mutations/index.js b/server/orchestrators/graphqlOrchestrator/src/schema/mutations/index.js index e0cb3b2..c1be869 100644 --- a/server/orchestrators/graphqlOrchestrator/src/schema/mutations/index.js +++ b/server/orchestrators/graphqlOrchestrator/src/schema/mutations/index.js @@ -65,18 +65,8 @@ const mutations = ` access_token: String ): SubCommentOutputDelete - create_log( - path: String - username: String - email: String - status: String - api_access_time: Float - method: String - status_code: Int - ): Log - delete_log( - id: Int + id: String ): LogOutputDelete reset_logs: LogOutputReset diff --git a/server/services/logs/package.json b/server/services/logs/package.json index 2e25bd1..b2e7dbf 100644 --- a/server/services/logs/package.json +++ b/server/services/logs/package.json @@ -4,7 +4,7 @@ "description": "", "main": "index.js", "scripts": { - "start": "nodemon src", + "start": "node src", "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [], diff --git a/server/services/users/package.json b/server/services/users/package.json index fd191f0..2968c77 100644 --- a/server/services/users/package.json +++ b/server/services/users/package.json @@ -16,11 +16,8 @@ "pg": "^8.5.1", "sequelize": "^6.3.5" }, - "devDependencies": { - "nodemon": "^2.0.6" - }, "scripts": { - "start": "nodemon src", + "start": "node src", "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [], From 2211094c956a47c013f04570cbfa227d8f941551 Mon Sep 17 00:00:00 2001 From: Alfian Tirta Date: Tue, 1 Dec 2020 14:08:17 +0700 Subject: [PATCH 27/37] modified --- server/.gitignore | 2 -- server/orchestrators/graphqlOrchestrator/.gitignore | 2 ++ server/services/logs/.gitignore | 2 ++ server/services/users/.gitignore | 2 ++ 4 files changed, 6 insertions(+), 2 deletions(-) delete mode 100644 server/.gitignore create mode 100644 server/orchestrators/graphqlOrchestrator/.gitignore create mode 100644 server/services/logs/.gitignore create mode 100644 server/services/users/.gitignore diff --git a/server/.gitignore b/server/.gitignore deleted file mode 100644 index 790c219..0000000 --- a/server/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -node_modules/ -.env* diff --git a/server/orchestrators/graphqlOrchestrator/.gitignore b/server/orchestrators/graphqlOrchestrator/.gitignore new file mode 100644 index 0000000..37d7e73 --- /dev/null +++ b/server/orchestrators/graphqlOrchestrator/.gitignore @@ -0,0 +1,2 @@ +node_modules +.env diff --git a/server/services/logs/.gitignore b/server/services/logs/.gitignore new file mode 100644 index 0000000..37d7e73 --- /dev/null +++ b/server/services/logs/.gitignore @@ -0,0 +1,2 @@ +node_modules +.env diff --git a/server/services/users/.gitignore b/server/services/users/.gitignore new file mode 100644 index 0000000..37d7e73 --- /dev/null +++ b/server/services/users/.gitignore @@ -0,0 +1,2 @@ +node_modules +.env From f3c9a1518ce49377887ba32ca02ea2c29d6197c8 Mon Sep 17 00:00:00 2001 From: Alfian Tirta Date: Tue, 1 Dec 2020 15:24:29 +0700 Subject: [PATCH 28/37] deployed users api to https://blog-users-api-alftirta.herokuapp.com --- server/services/users/Procfile | 1 + server/services/users/package.json | 7 +++++-- server/services/users/src/config/config.js | 4 ++-- server/services/users/src/models/index.js | 5 +++-- 4 files changed, 11 insertions(+), 6 deletions(-) create mode 100644 server/services/users/Procfile diff --git a/server/services/users/Procfile b/server/services/users/Procfile new file mode 100644 index 0000000..063b78f --- /dev/null +++ b/server/services/users/Procfile @@ -0,0 +1 @@ +web: npm start diff --git a/server/services/users/package.json b/server/services/users/package.json index 2968c77..a360ab3 100644 --- a/server/services/users/package.json +++ b/server/services/users/package.json @@ -17,10 +17,13 @@ "sequelize": "^6.3.5" }, "scripts": { - "start": "node src", + "start": "node ./src/index.js", "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [], "author": "", - "license": "ISC" + "license": "ISC", + "engines": { + "node": "14.11.0" + } } diff --git a/server/services/users/src/config/config.js b/server/services/users/src/config/config.js index 6dd088a..2426cfe 100644 --- a/server/services/users/src/config/config.js +++ b/server/services/users/src/config/config.js @@ -6,7 +6,7 @@ module.exports = { "password": process.env.DB_PASSWORD, "database": process.env.DB_NAME, "host": process.env.DB_HOST, - "dialect": process.env.DB_DIALECT, + "dialect": "postgres", }, "test": { "username": "root", @@ -20,6 +20,6 @@ module.exports = { "password": process.env.DB_PASSWORD, "database": process.env.DB_NAME, "host": process.env.DB_HOST, - "dialect": process.env.DB_DIALECT, + "dialect": "postgres", } } diff --git a/server/services/users/src/models/index.js b/server/services/users/src/models/index.js index f65e10b..2e5d586 100644 --- a/server/services/users/src/models/index.js +++ b/server/services/users/src/models/index.js @@ -10,10 +10,11 @@ const db = {}; let sequelize; if (config.use_env_variable) { - sequelize = new Sequelize(process.env[config.use_env_variable], config); + // sequelize = new Sequelize(process.env[config.use_env_variable], config); + sequelize = new Sequelize(`postgres://${process.env.DB_USERNAME}:${process.env.DB_PASSWORD}@rosie.db.elephantsql.com:5432/${process.env.DB_NAME}`); } else { // sequelize = new Sequelize(config.database, config.username, config.password, config); - sequelize = new Sequelize(process.env.ELEPHANTSQL_URL); + sequelize = new Sequelize(`postgres://${process.env.DB_USERNAME}:${process.env.DB_PASSWORD}@rosie.db.elephantsql.com:5432/${process.env.DB_NAME}`); } fs From 9c8af8e9af52368981d8306431a3efd5e53f7925 Mon Sep 17 00:00:00 2001 From: Alfian Tirta Date: Tue, 1 Dec 2020 15:41:07 +0700 Subject: [PATCH 29/37] changed logs localhost to online in users api; deployed logs api to https://blog-logs-api-alftirta.herokuapp.com/logs --- server/services/users/src/helpers/logger.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/services/users/src/helpers/logger.js b/server/services/users/src/helpers/logger.js index 6e49a88..cb17880 100644 --- a/server/services/users/src/helpers/logger.js +++ b/server/services/users/src/helpers/logger.js @@ -31,7 +31,7 @@ async function log(path, user_object, access_token, request_start_time, request_ } try { - await axios.post('http://localhost:3002/logs', { + await axios.post(process.env.LOGS_URL, { path, user_detail, api_access_time, From 5af816aa5e1f0fffe295a1b086088ed212c6009a Mon Sep 17 00:00:00 2001 From: Alfian Tirta Date: Tue, 1 Dec 2020 16:07:04 +0700 Subject: [PATCH 30/37] made logger url dynamic between production and development stage --- server/services/users/src/helpers/logger.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/server/services/users/src/helpers/logger.js b/server/services/users/src/helpers/logger.js index cb17880..ba033d4 100644 --- a/server/services/users/src/helpers/logger.js +++ b/server/services/users/src/helpers/logger.js @@ -1,6 +1,7 @@ const { User } = require('../models'); const { verify_jwt_token } = require('./jwt'); const axios = require('axios'); +const env = process.env.NODE_ENV || 'development'; async function log(path, user_object, access_token, request_start_time, request_object, response_object) { const api_access_time = Date.now() - request_start_time; @@ -31,7 +32,13 @@ async function log(path, user_object, access_token, request_start_time, request_ } try { - await axios.post(process.env.LOGS_URL, { + let logs_url; + if (env === 'production') { + logs_url = process.env.LOGS_URL; + } else { + logs_url = 'http://localhost:3002/logs'; + } + await axios.post(logs_url, { path, user_detail, api_access_time, From 4cc5a3cb59202a828327364f8f7244293da63ca5 Mon Sep 17 00:00:00 2001 From: Alfian Tirta Date: Tue, 1 Dec 2020 16:42:13 +0700 Subject: [PATCH 31/37] added get all users feature; updated users api doc --- .../users/src/controllers/UserController.js | 41 +++++++- .../services/users/src/routes/users/index.js | 1 + users_api.md | 95 ++++++++++++++++++- 3 files changed, 135 insertions(+), 2 deletions(-) diff --git a/server/services/users/src/controllers/UserController.js b/server/services/users/src/controllers/UserController.js index 4b80d4c..fb52d7a 100644 --- a/server/services/users/src/controllers/UserController.js +++ b/server/services/users/src/controllers/UserController.js @@ -1,4 +1,4 @@ -const { User } = require('../models'); +const { User, Post, Like, Comment, SubComment } = require('../models'); const { compare_bcrypt_password } = require('../helpers/bcrypt'); const { generate_jwt_token, verify_jwt_token } = require('../helpers/jwt'); const errorHandler = require('../helpers/errorHandler'); @@ -6,6 +6,45 @@ const sendEmail = require('../helpers/mailgun'); const log = require('../helpers/logger'); class UserController { + static async read(ctx) { + const start_time = Date.now(); + try { + const all_users = await User.findAll({ + include: [{ + model: Post, + include: [{ + model: Like, + }, { + model: Comment, + include: [SubComment], + }], + }], + }); + ctx.response.status = 200; + ctx.response.body = all_users; + log( + `${ctx.request.host}${ctx.request.url}`, + null, + ctx.request.header.access_token, + start_time, + ctx.request, + ctx.response, + ); + } catch(err) { + const { status, errors } = errHandler(err); + ctx.response.status = status; + ctx.response.body = errors; + log( + `${ctx.request.host}${ctx.request.url}`, + null, + ctx.request.header.access_token, + start_time, + ctx.request, + ctx.response, + ); + } + } + static async register(ctx) { const start_time = Date.now(); const { username, email, password } = ctx.request.body; diff --git a/server/services/users/src/routes/users/index.js b/server/services/users/src/routes/users/index.js index f16cb44..94f6ebe 100644 --- a/server/services/users/src/routes/users/index.js +++ b/server/services/users/src/routes/users/index.js @@ -2,6 +2,7 @@ const Controller = require('../../controllers/UserController'); function usersRoute(router) { router + .get('/users', Controller.read) .post('/users/register', Controller.register) .get('/users/verify', Controller.verify) .post('/users/login', Controller.login); diff --git a/users_api.md b/users_api.md index 912dc63..8220658 100644 --- a/users_api.md +++ b/users_api.md @@ -5,7 +5,7 @@ This document explains the Users services. The Users service has : -* RESTful endpoints for user registration, verification, and login. +* RESTful endpoints for user registration, verification, and login; and get all users data. * RESTful endpoints for CRUD, search, find-by-id, and find-by-user-id operations of blog posts. * RESTful endpoints for Create and Delete operations of likes, comments, and sub comments. * JSON formatted response. @@ -14,6 +14,7 @@ The Users service has : ## Endpoints ``` + - GET /users - POST /users/register - GET /users/verify?token= - POST /users/login @@ -37,6 +38,98 @@ The Users service has : ``` ## RESTful endpoints +### GET /posts + +> Get all blog posts + +_Request Header_ +``` +{ + "access_token": "" +} +*tidak wajib, hanya untuk keperluan logging data user +``` + +_Request Body_ +``` +not needed +``` + +_Response (200 - OK)_ +``` +[ + { + "id": , + "username": "", + "email": "", + "password": "", + "status": "", + "createdAt": "", + "updatedAt": "", + "Posts": [ + { + "id": , + "title": "", + "content": "", + "UserId": , + "createdAt": "", + "updatedAt": "", + "Likes": [ + { + "id": , + "PostId": , + "UserId": + }, + ... + ], + "Comments": [ + { + "id": , + "content": "", + "PostId": , + "UserId": , + "SubComments": [ + { + "id": , + "content": "", + "CommentId": , + "UserId": + }, + ... + ] + }, + ... + ] + }, + ... + ] + }, + ... +] +``` + +_Response (400 - Bad Request)_ +``` +[ + "", + ... +] +``` + +_Response (401 - Unauthorized)_ +``` +[ + "The user is not authenticated." +] +``` + +_Response (500 - Internal Server Error)_ +``` +[ + "Internal Server Error" +] +``` +--- ### POST /users/register > Register user From 777fc9474cc96e0adf90dcfb28dd87b924fda797 Mon Sep 17 00:00:00 2001 From: Alfian Tirta Date: Tue, 1 Dec 2020 17:19:49 +0700 Subject: [PATCH 32/37] connected get all users feature to graphqlOrchestrator with caching using redis --- .../src/dataSources/usersAPI.js | 9 +++++- .../src/resolvers/index.js | 12 ++++---- .../src/resolvers/mutations/index.js | 30 +++++++++---------- .../src/resolvers/queries/index.js | 17 +++++++++-- .../src/schema/models/index.js | 1 + .../src/schema/queries/index.js | 4 +++ 6 files changed, 50 insertions(+), 23 deletions(-) diff --git a/server/orchestrators/graphqlOrchestrator/src/dataSources/usersAPI.js b/server/orchestrators/graphqlOrchestrator/src/dataSources/usersAPI.js index 6dbd855..de898c8 100644 --- a/server/orchestrators/graphqlOrchestrator/src/dataSources/usersAPI.js +++ b/server/orchestrators/graphqlOrchestrator/src/dataSources/usersAPI.js @@ -6,7 +6,14 @@ class UsersAPI extends RESTDataSource { this.baseURL = 'http://localhost:3001'; } - // handle user registration, verification, and login features : + // handle user features : + async readUsers(token) { + return this.get(`/users`, null, { + headers: { + access_token: token, + }, + }); + } async registerUser(user) { return this.post('/users/register', user); } diff --git a/server/orchestrators/graphqlOrchestrator/src/resolvers/index.js b/server/orchestrators/graphqlOrchestrator/src/resolvers/index.js index 516f6e0..9a5ee6b 100644 --- a/server/orchestrators/graphqlOrchestrator/src/resolvers/index.js +++ b/server/orchestrators/graphqlOrchestrator/src/resolvers/index.js @@ -1,4 +1,5 @@ const { + users, posts, search_posts, post_by_id, @@ -25,11 +26,12 @@ const { const resolvers = { Query: { - posts, - search_posts, - post_by_id, - posts_by_user_id, - logs, + users, + posts, + search_posts, + post_by_id, + posts_by_user_id, + logs, }, Mutation: { register_user, diff --git a/server/orchestrators/graphqlOrchestrator/src/resolvers/mutations/index.js b/server/orchestrators/graphqlOrchestrator/src/resolvers/mutations/index.js index dd98b2f..cae5f1c 100644 --- a/server/orchestrators/graphqlOrchestrator/src/resolvers/mutations/index.js +++ b/server/orchestrators/graphqlOrchestrator/src/resolvers/mutations/index.js @@ -1,7 +1,7 @@ const Redis = require('ioredis'); const redis = new Redis(); -// handle user registration, verification, and login features : +// handle user mutations : async function register_user(_, user, { dataSources }) { return dataSources.usersAPI.registerUser(user); } @@ -12,51 +12,51 @@ async function login_user(_, user, { dataSources }) { return dataSources.usersAPI.loginUser(user); } -// handle post features : +// handle post mutations : async function create_post(_, { title, content, access_token }, { dataSources }) { - await redis.del('posts'); + await redis.del('users', 'posts'); return dataSources.usersAPI.createPost({ title, content }, access_token); } async function update_post(_, { id, title, content, access_token }, { dataSources }) { - await redis.del('posts'); + await redis.del('users', 'posts'); return dataSources.usersAPI.updatePost(id, { title, content }, access_token); } async function delete_post(_, { id, access_token }, { dataSources }) { - await redis.del('posts'); + await redis.del('users', 'posts'); return dataSources.usersAPI.deletePost(id, access_token); } -// handle like-unlike features : +// handle like-unlike mutations : async function create_like(_, { PostId, access_token }, { dataSources }) { - await redis.del('posts'); + await redis.del('users', 'posts'); return dataSources.usersAPI.createLike({ PostId }, access_token); } async function delete_like(_, { id, access_token }, { dataSources }) { - await redis.del('posts'); + await redis.del('users', 'posts'); return dataSources.usersAPI.deleteLike(id, access_token); } -// handle comment features : +// handle comment mutations : async function create_comment(_, { content, PostId, access_token }, { dataSources }) { - await redis.del('posts'); + await redis.del('users', 'posts'); return dataSources.usersAPI.createComment({ content, PostId }, access_token); } async function delete_comment(_, { id, access_token }, { dataSources }) { - await redis.del('posts'); + await redis.del('users', 'posts'); return dataSources.usersAPI.deleteComment(id, access_token); } -// handle sub comment features : +// handle sub comment mutations : async function create_sub_comment(_, { content, CommentId, access_token }, { dataSources }) { - await redis.del('posts'); + await redis.del('users', 'posts'); return dataSources.usersAPI.createSubComment({ content, CommentId }, access_token); } async function delete_sub_comment(_, { id, access_token }, { dataSources }) { - await redis.del('posts'); + await redis.del('users', 'posts'); return dataSources.usersAPI.deleteSubComment(id, access_token); } -// handle log features : +// handle log mutations : async function delete_log(_, { id }, { dataSources }) { return dataSources.logsAPI.deleteLog(id); } diff --git a/server/orchestrators/graphqlOrchestrator/src/resolvers/queries/index.js b/server/orchestrators/graphqlOrchestrator/src/resolvers/queries/index.js index b339968..dad2499 100644 --- a/server/orchestrators/graphqlOrchestrator/src/resolvers/queries/index.js +++ b/server/orchestrators/graphqlOrchestrator/src/resolvers/queries/index.js @@ -1,7 +1,19 @@ const Redis = require('ioredis'); const redis = new Redis(); -// handle post features : +// handle user query : +async function users(_, { access_token }, { dataSources }) { + const cached_users = await redis.get('users'); + if (cached_users) { + return JSON.parse(cached_users); + } else { + const users = await dataSources.usersAPI.readUsers(access_token); + await redis.set('users', JSON.stringify(users)); + return users; + } +} + +// handle post queries : async function posts(_, { access_token }, { dataSources }) { const cached_posts = await redis.get('posts'); if (cached_posts) { @@ -22,12 +34,13 @@ async function posts_by_user_id(_, { id, access_token }, { dataSources }) { return dataSources.usersAPI.findPostsByUserId(id, access_token); } -// handle log feature : +// handle log query : async function logs(_, __, { dataSources }) { return dataSources.logsAPI.readLogs(); } module.exports = { + users, posts, search_posts, post_by_id, diff --git a/server/orchestrators/graphqlOrchestrator/src/schema/models/index.js b/server/orchestrators/graphqlOrchestrator/src/schema/models/index.js index fff14a5..2d79e15 100644 --- a/server/orchestrators/graphqlOrchestrator/src/schema/models/index.js +++ b/server/orchestrators/graphqlOrchestrator/src/schema/models/index.js @@ -7,6 +7,7 @@ const models = ` status: String createdAt: String updatedAt: String + Posts: [Post] } type UserOutputRegister { diff --git a/server/orchestrators/graphqlOrchestrator/src/schema/queries/index.js b/server/orchestrators/graphqlOrchestrator/src/schema/queries/index.js index 3930783..2dd3ee9 100644 --- a/server/orchestrators/graphqlOrchestrator/src/schema/queries/index.js +++ b/server/orchestrators/graphqlOrchestrator/src/schema/queries/index.js @@ -1,5 +1,9 @@ const queries = ` type Query { + users( + access_token: String + ): [User] + posts( access_token: String ): [Post] From 4e32fd6b7e64cf1d390df11b9a35b95d679fe7fc Mon Sep 17 00:00:00 2001 From: Alfian Tirta Date: Tue, 1 Dec 2020 17:28:02 +0700 Subject: [PATCH 33/37] deleted unused console.log line --- server/services/users/src/helpers/logger.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/services/users/src/helpers/logger.js b/server/services/users/src/helpers/logger.js index ba033d4..e0d3bbd 100644 --- a/server/services/users/src/helpers/logger.js +++ b/server/services/users/src/helpers/logger.js @@ -15,7 +15,7 @@ async function log(path, user_object, access_token, request_start_time, request_ const user = await User.findOne({ where: user_object }); user_detail = user; } else if (access_token) { - const decoded_user_data = verify_jwt_token(access_token); console.log('hasilnya gimana guyssss =====', decoded_user_data); + const decoded_user_data = verify_jwt_token(access_token); const { id, username, email, status } = decoded_user_data; const user = await User.findOne({ where: { id, username, email, status }}); user_detail = user; From bcf7eaaeb1e28961c7dd275a158be4e523701ff4 Mon Sep 17 00:00:00 2001 From: Alfian Tirta Date: Tue, 1 Dec 2020 17:41:40 +0700 Subject: [PATCH 34/37] made base url and port dynamic between development and production stage --- .../graphqlOrchestrator/src/dataSources/logsAPI.js | 11 ++++++++++- .../graphqlOrchestrator/src/dataSources/usersAPI.js | 11 ++++++++++- server/orchestrators/graphqlOrchestrator/src/index.js | 2 +- 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/server/orchestrators/graphqlOrchestrator/src/dataSources/logsAPI.js b/server/orchestrators/graphqlOrchestrator/src/dataSources/logsAPI.js index 63656bd..f79e26b 100644 --- a/server/orchestrators/graphqlOrchestrator/src/dataSources/logsAPI.js +++ b/server/orchestrators/graphqlOrchestrator/src/dataSources/logsAPI.js @@ -1,9 +1,18 @@ const { RESTDataSource } = require('apollo-datasource-rest'); +const env = process.env.NODE_ENV || 'development'; class LogsAPI extends RESTDataSource { constructor() { super(); - this.baseURL = 'http://localhost:3002'; + this.baseURL = this.chooseBaseURL(env); + } + + chooseBaseURL(env) { + if (env === 'production') { + return 'https://blog-users-api-alftirta.herokuapp.com'; + } else { + return 'http://localhost:3001'; + } } async readLogs() { diff --git a/server/orchestrators/graphqlOrchestrator/src/dataSources/usersAPI.js b/server/orchestrators/graphqlOrchestrator/src/dataSources/usersAPI.js index de898c8..c0bd734 100644 --- a/server/orchestrators/graphqlOrchestrator/src/dataSources/usersAPI.js +++ b/server/orchestrators/graphqlOrchestrator/src/dataSources/usersAPI.js @@ -1,9 +1,18 @@ const { RESTDataSource } = require('apollo-datasource-rest'); +const env = process.env.NODE_ENV || 'development'; class UsersAPI extends RESTDataSource { constructor() { super(); - this.baseURL = 'http://localhost:3001'; + this.baseURL = this.chooseBaseURL(env); + } + + chooseBaseURL(env) { + if (env === 'production') { + return 'https://blog-users-api-alftirta.herokuapp.com'; + } else { + return 'http://localhost:3001'; + } } // handle user features : diff --git a/server/orchestrators/graphqlOrchestrator/src/index.js b/server/orchestrators/graphqlOrchestrator/src/index.js index 000b7c2..78c6a8c 100644 --- a/server/orchestrators/graphqlOrchestrator/src/index.js +++ b/server/orchestrators/graphqlOrchestrator/src/index.js @@ -18,7 +18,7 @@ const server = new ApolloServer({ }); server - .listen(3000) + .listen({ port: process.env.PORT || 3000 }) .then(({ url }) => { console.log(`graphqlOrchestrator running at ${url}`); }) From 7d24a59dfa8c5e89e89e1b6d3065bf236853ea5b Mon Sep 17 00:00:00 2001 From: Alfian Tirta Date: Tue, 1 Dec 2020 18:00:10 +0700 Subject: [PATCH 35/37] deployed graphqlOrchestrator with playground enabled --- server/orchestrators/graphqlOrchestrator/package.json | 2 +- server/orchestrators/graphqlOrchestrator/src/index.js | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/server/orchestrators/graphqlOrchestrator/package.json b/server/orchestrators/graphqlOrchestrator/package.json index cd52749..f56cb48 100644 --- a/server/orchestrators/graphqlOrchestrator/package.json +++ b/server/orchestrators/graphqlOrchestrator/package.json @@ -4,7 +4,7 @@ "description": "", "main": "index.js", "scripts": { - "start": "nodemon src", + "start": "node src", "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [], diff --git a/server/orchestrators/graphqlOrchestrator/src/index.js b/server/orchestrators/graphqlOrchestrator/src/index.js index 78c6a8c..42f2d99 100644 --- a/server/orchestrators/graphqlOrchestrator/src/index.js +++ b/server/orchestrators/graphqlOrchestrator/src/index.js @@ -15,6 +15,8 @@ const server = new ApolloServer({ logsAPI: new LogsAPI(), }; }, + introspection: true, + playground: true, }); server From c7e004df6eb2c08da4d992a9b537e4ba28fa50d3 Mon Sep 17 00:00:00 2001 From: Alfian Tirta Date: Tue, 1 Dec 2020 23:45:28 +0700 Subject: [PATCH 36/37] set redis only on development stage --- .../src/dataSources/logsAPI.js | 2 +- .../src/resolvers/mutations/index.js | 46 +++++++++++++++---- .../src/resolvers/queries/index.js | 33 ++++++++----- 3 files changed, 59 insertions(+), 22 deletions(-) diff --git a/server/orchestrators/graphqlOrchestrator/src/dataSources/logsAPI.js b/server/orchestrators/graphqlOrchestrator/src/dataSources/logsAPI.js index f79e26b..b426801 100644 --- a/server/orchestrators/graphqlOrchestrator/src/dataSources/logsAPI.js +++ b/server/orchestrators/graphqlOrchestrator/src/dataSources/logsAPI.js @@ -11,7 +11,7 @@ class LogsAPI extends RESTDataSource { if (env === 'production') { return 'https://blog-users-api-alftirta.herokuapp.com'; } else { - return 'http://localhost:3001'; + return 'http://localhost:3002'; } } diff --git a/server/orchestrators/graphqlOrchestrator/src/resolvers/mutations/index.js b/server/orchestrators/graphqlOrchestrator/src/resolvers/mutations/index.js index cae5f1c..3ae87b6 100644 --- a/server/orchestrators/graphqlOrchestrator/src/resolvers/mutations/index.js +++ b/server/orchestrators/graphqlOrchestrator/src/resolvers/mutations/index.js @@ -1,58 +1,86 @@ const Redis = require('ioredis'); const redis = new Redis(); +const env = process.env.NODE_ENV || 'development'; // handle user mutations : async function register_user(_, user, { dataSources }) { + if (env === 'development') { + await redis.del('users', 'posts'); + } return dataSources.usersAPI.registerUser(user); } async function verify_user(_, { token }, { dataSources }) { + if (env === 'development') { + await redis.del('users', 'posts'); + } return dataSources.usersAPI.verifyUser(token); } async function login_user(_, user, { dataSources }) { + if (env === 'development') { + await redis.del('users', 'posts'); + } return dataSources.usersAPI.loginUser(user); } // handle post mutations : async function create_post(_, { title, content, access_token }, { dataSources }) { - await redis.del('users', 'posts'); + if (env === 'development') { + await redis.del('users', 'posts'); + } return dataSources.usersAPI.createPost({ title, content }, access_token); } async function update_post(_, { id, title, content, access_token }, { dataSources }) { - await redis.del('users', 'posts'); + if (env === 'development') { + await redis.del('users', 'posts'); + } return dataSources.usersAPI.updatePost(id, { title, content }, access_token); } async function delete_post(_, { id, access_token }, { dataSources }) { - await redis.del('users', 'posts'); + if (env === 'development') { + await redis.del('users', 'posts'); + } return dataSources.usersAPI.deletePost(id, access_token); } // handle like-unlike mutations : async function create_like(_, { PostId, access_token }, { dataSources }) { - await redis.del('users', 'posts'); + if (env === 'development') { + await redis.del('users', 'posts'); + } return dataSources.usersAPI.createLike({ PostId }, access_token); } async function delete_like(_, { id, access_token }, { dataSources }) { - await redis.del('users', 'posts'); + if (env === 'development') { + await redis.del('users', 'posts'); + } return dataSources.usersAPI.deleteLike(id, access_token); } // handle comment mutations : async function create_comment(_, { content, PostId, access_token }, { dataSources }) { - await redis.del('users', 'posts'); + if (env === 'development') { + await redis.del('users', 'posts'); + } return dataSources.usersAPI.createComment({ content, PostId }, access_token); } async function delete_comment(_, { id, access_token }, { dataSources }) { - await redis.del('users', 'posts'); + if (env === 'development') { + await redis.del('users', 'posts'); + } return dataSources.usersAPI.deleteComment(id, access_token); } // handle sub comment mutations : async function create_sub_comment(_, { content, CommentId, access_token }, { dataSources }) { - await redis.del('users', 'posts'); + if (env === 'development') { + await redis.del('users', 'posts'); + } return dataSources.usersAPI.createSubComment({ content, CommentId }, access_token); } async function delete_sub_comment(_, { id, access_token }, { dataSources }) { - await redis.del('users', 'posts'); + if (env === 'development') { + await redis.del('users', 'posts'); + } return dataSources.usersAPI.deleteSubComment(id, access_token); } diff --git a/server/orchestrators/graphqlOrchestrator/src/resolvers/queries/index.js b/server/orchestrators/graphqlOrchestrator/src/resolvers/queries/index.js index dad2499..9b26a41 100644 --- a/server/orchestrators/graphqlOrchestrator/src/resolvers/queries/index.js +++ b/server/orchestrators/graphqlOrchestrator/src/resolvers/queries/index.js @@ -1,27 +1,36 @@ const Redis = require('ioredis'); const redis = new Redis(); +const env = process.env.NODE_ENV || 'development'; // handle user query : async function users(_, { access_token }, { dataSources }) { - const cached_users = await redis.get('users'); - if (cached_users) { - return JSON.parse(cached_users); + if (env === 'production') { + return dataSources.usersAPI.readUsers(access_token); } else { - const users = await dataSources.usersAPI.readUsers(access_token); - await redis.set('users', JSON.stringify(users)); - return users; + const cached_users = await redis.get('users'); + if (cached_users) { + return JSON.parse(cached_users); + } else { + const users = await dataSources.usersAPI.readUsers(access_token); + await redis.set('users', JSON.stringify(users)); + return users; + } } } // handle post queries : async function posts(_, { access_token }, { dataSources }) { - const cached_posts = await redis.get('posts'); - if (cached_posts) { - return JSON.parse(cached_posts); + if (env === 'production') { + return dataSources.usersAPI.readPosts(access_token); } else { - const posts = await dataSources.usersAPI.readPosts(access_token); - await redis.set('posts', JSON.stringify(posts)); - return posts; + const cached_posts = await redis.get('posts'); + if (cached_posts) { + return JSON.parse(cached_posts); + } else { + const posts = await dataSources.usersAPI.readPosts(access_token); + await redis.set('posts', JSON.stringify(posts)); + return posts; + } } } async function search_posts(_, { title, sort, order, access_token }, { dataSources }) { From acd77c206bf3d207914b4185c7ee22cf6de870c7 Mon Sep 17 00:00:00 2001 From: Alfian Tirta Date: Tue, 1 Dec 2020 23:54:35 +0700 Subject: [PATCH 37/37] fixed typo: changed "users" to "logs" in logsAPI.js file --- .../graphqlOrchestrator/src/dataSources/logsAPI.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/orchestrators/graphqlOrchestrator/src/dataSources/logsAPI.js b/server/orchestrators/graphqlOrchestrator/src/dataSources/logsAPI.js index b426801..cbb38b7 100644 --- a/server/orchestrators/graphqlOrchestrator/src/dataSources/logsAPI.js +++ b/server/orchestrators/graphqlOrchestrator/src/dataSources/logsAPI.js @@ -9,7 +9,7 @@ class LogsAPI extends RESTDataSource { chooseBaseURL(env) { if (env === 'production') { - return 'https://blog-users-api-alftirta.herokuapp.com'; + return 'https://blog-logs-api-alftirta.herokuapp.com'; } else { return 'http://localhost:3002'; }