From 7b8f0ea16b26d796f2c3a6bb990bd6f1a1a3d402 Mon Sep 17 00:00:00 2001 From: Robus Gauli Date: Wed, 8 Apr 2020 14:40:42 +0545 Subject: [PATCH 01/24] Configure mysql client and remove postgres as a deps --- package.json | 2 +- .../20170107202211_create_users_table.js | 9 +- yarn.lock | 125 +++--------------- 3 files changed, 25 insertions(+), 111 deletions(-) diff --git a/package.json b/package.json index 39ce622..2972387 100644 --- a/package.json +++ b/package.json @@ -60,7 +60,7 @@ "knex": "^0.20.12", "lodash": "^4.17.13", "morgan": "^1.10.0", - "pg": "^7.18.2", + "mysql": "^2.18.1", "serve-favicon": "^2.5.0", "swagger-jsdoc": "^3.5.0", "swagger-ui-dist": "^3.25.0", diff --git a/src/migrations/20170107202211_create_users_table.js b/src/migrations/20170107202211_create_users_table.js index 60efabd..11eeddc 100644 --- a/src/migrations/20170107202211_create_users_table.js +++ b/src/migrations/20170107202211_create_users_table.js @@ -5,14 +5,9 @@ * @returns {Promise} */ export function up(knex) { - return knex.schema.createTable('users', table => { + return knex.schema.createTable('users', (table) => { table.increments(); - table - .timestamp('created_at') - .notNull() - .defaultTo(knex.raw('now()')); - table.timestamp('updated_at').notNull(); - table.string('name').notNull(); + table.string('name').default('defualt name'); }); } diff --git a/yarn.lock b/yarn.lock index f59ed91..3900fdf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1229,6 +1229,11 @@ basic-auth@~2.0.1: dependencies: safe-buffer "5.1.2" +bignumber.js@9.0.0: + version "9.0.0" + resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.0.0.tgz#805880f84a329b5eac6e7cb6f8274b6d82bdf075" + integrity sha512-t/OYhhJ2SD+YGBQcjY8GzzDHEk9f3nerxjtfa6tlMXfe7frs/WozhvCNoGvpM0P3bNf3Gq5ZRMlGr5f3r4/N8A== + binary-extensions@^1.0.0: version "1.13.1" resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.13.1.tgz#598afe54755b2868a5330d2aff9d4ebb53209b65" @@ -1353,11 +1358,6 @@ buffer-from@^1.0.0: resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A== -buffer-writer@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/buffer-writer/-/buffer-writer-2.0.0.tgz#ce7eb81a38f7829db09c873f2fbb792c0c98ec04" - integrity sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw== - bytes@3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048" @@ -4134,6 +4134,16 @@ mute-stream@0.0.8: resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d" integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA== +mysql@^2.18.1: + version "2.18.1" + resolved "https://registry.yarnpkg.com/mysql/-/mysql-2.18.1.tgz#2254143855c5a8c73825e4522baf2ea021766717" + integrity sha512-Bca+gk2YWmqp2Uf6k5NFEurwY/0td0cpebAucFpY/3jhrwrVGuxU2uQFCHjU19SJfje0yQvi+rVWdq78hR5lig== + dependencies: + bignumber.js "9.0.0" + readable-stream "2.3.7" + safe-buffer "5.1.2" + sqlstring "2.3.1" + nan@^2.12.1: version "2.14.0" resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.0.tgz#7818f722027b2459a86f0295d434d1fc2336c52c" @@ -4553,11 +4563,6 @@ package-json@^4.0.0: registry-url "^3.0.3" semver "^5.1.0" -packet-reader@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/packet-reader/-/packet-reader-1.0.0.tgz#9238e5480dedabacfe1fe3f2771063f164157d74" - integrity sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ== - parent-module@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" @@ -4666,63 +4671,11 @@ pathval@^1.1.0: resolved "https://registry.yarnpkg.com/pathval/-/pathval-1.1.0.tgz#b942e6d4bde653005ef6b71361def8727d0645e0" integrity sha1-uULm1L3mUwBe9rcTYd74cn0GReA= -pg-connection-string@0.1.3: - version "0.1.3" - resolved "https://registry.yarnpkg.com/pg-connection-string/-/pg-connection-string-0.1.3.tgz#da1847b20940e42ee1492beaf65d49d91b245df7" - integrity sha1-2hhHsglA5C7hSSvq9l1J2RskXfc= - pg-connection-string@2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/pg-connection-string/-/pg-connection-string-2.1.0.tgz#e07258f280476540b24818ebb5dca29e101ca502" integrity sha512-bhlV7Eq09JrRIvo1eKngpwuqKtJnNhZdpdOlvrPrA4dxqXPjxSrbNrfnIDmTpwMyRszrcV4kU5ZA4mMsQUrjdg== -pg-int8@1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/pg-int8/-/pg-int8-1.0.1.tgz#943bd463bf5b71b4170115f80f8efc9a0c0eb78c" - integrity sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw== - -pg-packet-stream@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/pg-packet-stream/-/pg-packet-stream-1.1.0.tgz#e45c3ae678b901a2873af1e17b92d787962ef914" - integrity sha512-kRBH0tDIW/8lfnnOyTwKD23ygJ/kexQVXZs7gEyBljw4FYqimZFxnMMx50ndZ8In77QgfGuItS5LLclC2TtjYg== - -pg-pool@^2.0.10: - version "2.0.10" - resolved "https://registry.yarnpkg.com/pg-pool/-/pg-pool-2.0.10.tgz#842ee23b04e86824ce9d786430f8365082d81c4a" - integrity sha512-qdwzY92bHf3nwzIUcj+zJ0Qo5lpG/YxchahxIN8+ZVmXqkahKXsnl2aiJPHLYN9o5mB/leG+Xh6XKxtP7e0sjg== - -pg-types@^2.1.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/pg-types/-/pg-types-2.2.0.tgz#2d0250d636454f7cfa3b6ae0382fdfa8063254a3" - integrity sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA== - dependencies: - 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" - -pg@^7.18.2: - version "7.18.2" - resolved "https://registry.yarnpkg.com/pg/-/pg-7.18.2.tgz#4e219f05a00aff4db6aab1ba02f28ffa4513b0bb" - integrity sha512-Mvt0dGYMwvEADNKy5PMQGlzPudKcKKzJds/VbOeZJpb6f/pI3mmoXX0JksPgI3l3JPP/2Apq7F36O63J7mgveA== - dependencies: - buffer-writer "2.0.0" - packet-reader "1.0.0" - pg-connection-string "0.1.3" - pg-packet-stream "^1.1.0" - pg-pool "^2.0.10" - pg-types "^2.1.0" - pgpass "1.x" - semver "4.3.2" - -pgpass@1.x: - version "1.0.2" - resolved "https://registry.yarnpkg.com/pgpass/-/pgpass-1.0.2.tgz#2a7bb41b6065b67907e91da1b07c1847c877b306" - integrity sha1-Knu0G2BltnkH6R2hsHwYR8h3swY= - dependencies: - split "^1.0.0" - picomatch@^2.0.4, picomatch@^2.0.5, picomatch@^2.0.7: version "2.2.2" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.2.tgz#21f333e9b6b8eaff02468f5146ea406d345f4dad" @@ -4778,28 +4731,6 @@ posix-character-classes@^0.1.0: resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab" integrity sha1-AerA/jta9xoqbAL+q7jB/vfgDqs= -postgres-array@~2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/postgres-array/-/postgres-array-2.0.0.tgz#48f8fce054fbc69671999329b8834b772652d82e" - integrity sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA== - -postgres-bytea@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/postgres-bytea/-/postgres-bytea-1.0.0.tgz#027b533c0aa890e26d172d47cf9ccecc521acd35" - integrity sha1-AntTPAqokOJtFy1Hz5zOzFIazTU= - -postgres-date@~1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/postgres-date/-/postgres-date-1.0.4.tgz#1c2728d62ef1bff49abdd35c1f86d4bdf118a728" - integrity sha512-bESRvKVuTrjoBluEcpv2346+6kgB7UlnqWZsnbnCccTNq/pqfj1j6oBaN5+b/NrDXepYUT/HKadqv3iS9lJuVA== - -postgres-interval@^1.1.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/postgres-interval/-/postgres-interval-1.2.0.tgz#b460c82cb1587507788819a06aa0fffdb3544695" - integrity sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ== - dependencies: - xtend "^4.0.0" - prelude-ls@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" @@ -4917,7 +4848,7 @@ react-is@^16.8.1: resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== -readable-stream@^2.0.2, readable-stream@^2.3.5, readable-stream@^2.3.6: +readable-stream@2.3.7, readable-stream@^2.0.2, readable-stream@^2.3.5, readable-stream@^2.3.6: version "2.3.7" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== @@ -5214,11 +5145,6 @@ semver-regex@^2.0.0: resolved "https://registry.yarnpkg.com/semver-regex/-/semver-regex-2.0.0.tgz#a93c2c5844539a770233379107b38c7b4ac9d338" integrity sha512-mUdIBBvdn0PLOeP3TEkMH7HHeUP3GjsXCwKarjv/kGmUFOYg1VqEemKhoQpWMu6X2I8kHeuVdGibLGkVK+/5Qw== -semver@4.3.2: - version "4.3.2" - resolved "https://registry.yarnpkg.com/semver/-/semver-4.3.2.tgz#c7a07158a80bedd052355b770d82d6640f803be7" - integrity sha1-x6BxWKgL7dBSNVt3DYLWZA+AO+c= - semver@7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/semver/-/semver-7.0.0.tgz#5f3ca35761e47e05b206c6daff2cf814f0316b8e" @@ -5445,18 +5371,16 @@ split-string@^3.0.1, split-string@^3.0.2: dependencies: extend-shallow "^3.0.0" -split@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/split/-/split-1.0.1.tgz#605bd9be303aa59fb35f9229fbea0ddec9ea07d9" - integrity sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg== - dependencies: - through "2" - sprintf-js@~1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw= +sqlstring@2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/sqlstring/-/sqlstring-2.3.1.tgz#475393ff9e91479aea62dcaf0ca3d14983a7fb40" + integrity sha1-R1OT/56RR5rqYtyvDKPRSYOn+0A= + stack-trace@0.0.x: version "0.0.10" resolved "https://registry.yarnpkg.com/stack-trace/-/stack-trace-0.0.10.tgz#547c70b347e8d32b4e108ea1a2a159e5fdde19c0" @@ -5772,7 +5696,7 @@ text-table@^0.2.0: resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ= -through@2, through@^2.3.6: +through@^2.3.6: version "2.3.8" resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU= @@ -6202,11 +6126,6 @@ xregexp@^4.3.0: dependencies: "@babel/runtime-corejs3" "^7.8.3" -xtend@^4.0.0: - version "4.0.2" - resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" - integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== - y18n@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.0.tgz#95ef94f85ecc81d007c264e190a120f0a3c8566b" From 7d22fbb13ef8120db211c87b3066a9fb6b0c0a10 Mon Sep 17 00:00:00 2001 From: Robus Gauli Date: Wed, 8 Apr 2020 15:47:35 +0545 Subject: [PATCH 02/24] Add errors instance for token and network --- src/auth/auth.js | 72 +++++++++++++++++++++++++++++++++++++++++++ src/errors/error.js | 10 ++++++ src/errors/network.js | 23 ++++++++++++++ src/errors/token.js | 22 +++++++++++++ 4 files changed, 127 insertions(+) create mode 100644 src/auth/auth.js create mode 100644 src/errors/error.js create mode 100644 src/errors/network.js create mode 100644 src/errors/token.js diff --git a/src/auth/auth.js b/src/auth/auth.js new file mode 100644 index 0000000..dda4c8c --- /dev/null +++ b/src/auth/auth.js @@ -0,0 +1,72 @@ +import HttpStatus from 'http-status-codes'; + +import TokenError from '../errors/token'; +import NetworkError from '../errors/network'; +import * as userServices from '../services/user'; +import logger from '../utils/logger'; + +const EMPTY_TOKEN = 'Access token not provided.'; +const INTERNAL_ERROR = 'Internal Error'; + +/** + * Get token from header in request. + * + * @param {Object} req + */ +const getTokenFromHeaders = req => { + const { + headers: { authorization } + } = req; + + if (authorization && authorization.split(' ')[0] === 'Bearer') { + if (authorization.split(' ')[1] !== undefined) { + return authorization.split(' ')[1]; + } + } + logger.error(`Token Error: ${EMPTY_TOKEN}`); + + throw new TokenError({ + message: EMPTY_TOKEN, + code: HttpStatus.UNAUTHORIZED + }); +}; + +/** + * Validate token received in header. + * + * @param {Object} req + * @param {Object} res + * @param {Object} next + */ +export async function authenticateUser(req, res, next) { + try { + const token = getTokenFromHeaders(req); + const user = await userServices.fetchUserByToken(token); + + req.token = token; + req.currentUser = user.data; + next(); + } catch (error) { + if (error instanceof NetworkError) { + logger.error(`Network Error: ${error}`); + + return next( + new NetworkError({ + message: INTERNAL_ERROR, + code: HttpStatus.INTERNAL_SERVER_ERROR + }) + ); + } + + if (error instanceof TokenError) { + return next(error); + } + + return next( + new TokenError({ + code: 401, + message: 'Not Authorized' + }) + ); + } +} \ No newline at end of file diff --git a/src/errors/error.js b/src/errors/error.js new file mode 100644 index 0000000..4fd43da --- /dev/null +++ b/src/errors/error.js @@ -0,0 +1,10 @@ +/** + * Base class for error. + */ +class BaseClass extends Error { + constructor(message) { + super(message); + } +} + +export default BaseClass \ No newline at end of file diff --git a/src/errors/network.js b/src/errors/network.js new file mode 100644 index 0000000..472b640 --- /dev/null +++ b/src/errors/network.js @@ -0,0 +1,23 @@ + +import BaseError from './error'; + +/** + * Error class for Network error. + */ +class NetworkError extends BaseError { + /** + * Constructor of NetworkError. + * + * @param {Object} error + * @param {String} error.message + * @param {String} error.detail + * @param {Number} error.code + */ + constructor({ message = '', detail = '', code = 500 }) { + super(message); + this.detail = detail; + this.code = code; + } +} + +export default NetworkError; \ No newline at end of file diff --git a/src/errors/token.js b/src/errors/token.js new file mode 100644 index 0000000..433dfd8 --- /dev/null +++ b/src/errors/token.js @@ -0,0 +1,22 @@ +import BaseError from './error'; + +/** + * Error class for Token Error. + */ +class TokenError extends BaseError { + /** + * Constructor of NetworkError. + * + * @param {Object} error + * @param {String} error.message + * @param {String} error.details + * @param {Number} error.code + */ + constructor({ message = '', detail = '', code = 401 }) { + super(message); + this.detail = detail; + this.code = code; + } +} + +export default TokenError; \ No newline at end of file From cac594c022aa150ff82b0c34026c54d82d831f56 Mon Sep 17 00:00:00 2001 From: Robus Gauli Date: Wed, 8 Apr 2020 16:05:24 +0545 Subject: [PATCH 03/24] Integrat LMS authentication --- .env.example | 10 +++++++-- package.json | 1 + src/auth/{auth.js => index.js} | 38 ++++++++++++++++++++++++++-------- src/routes.js | 9 +++++++- src/utils/http.js | 28 +++++++++++++++++++++++++ yarn.lock | 21 +++++++++++++++++++ 6 files changed, 95 insertions(+), 12 deletions(-) rename src/auth/{auth.js => index.js} (63%) create mode 100644 src/utils/http.js diff --git a/.env.example b/.env.example index 417b55f..109623f 100644 --- a/.env.example +++ b/.env.example @@ -8,15 +8,21 @@ APP_HOST='127.0.0.1' LOG_LEVEL='debug' # Database -DB_CLIENT='pg' +DB_CLIENT='mysql' + # App Environment -DB_PORT='5432' +DB_PORT='3306' DB_HOST='localhost' DB_NAME='express' DB_USER='username' DB_PASSWORD='password' + +# Authenication parameters +AUTH_URL='localhost:5000/' +AUTH_CLIENT_ID='secret-client-id' + # Sentry # https://docs.sentry.io/quickstart SENTRY_DSN='' diff --git a/package.json b/package.json index 2972387..cdc2da6 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "@hapi/boom": "^9.1.0", "@hapi/joi": "^17.1.1", "@sentry/node": "^5.15.0", + "axios": "^0.19.2", "body-parser": "^1.19.0", "bookshelf": "^1.1.0", "bookshelf-virtuals-plugin": "^0.1.1", diff --git a/src/auth/auth.js b/src/auth/index.js similarity index 63% rename from src/auth/auth.js rename to src/auth/index.js index dda4c8c..dff6bd3 100644 --- a/src/auth/auth.js +++ b/src/auth/index.js @@ -2,7 +2,8 @@ import HttpStatus from 'http-status-codes'; import TokenError from '../errors/token'; import NetworkError from '../errors/network'; -import * as userServices from '../services/user'; + +import { http } from '../utils/http'; import logger from '../utils/logger'; const EMPTY_TOKEN = 'Access token not provided.'; @@ -13,9 +14,9 @@ const INTERNAL_ERROR = 'Internal Error'; * * @param {Object} req */ -const getTokenFromHeaders = req => { +const getTokenFromHeaders = (req) => { const { - headers: { authorization } + headers: { authorization }, } = req; if (authorization && authorization.split(' ')[0] === 'Bearer') { @@ -27,10 +28,27 @@ const getTokenFromHeaders = req => { throw new TokenError({ message: EMPTY_TOKEN, - code: HttpStatus.UNAUTHORIZED + code: HttpStatus.UNAUTHORIZED, }); }; +/** + * Fetch users from auth server from token. + * + * @param {String} token + * @throws NetworkError + */ +export function fetchUserByToken(token) { + return http + .get(`${process.env.AUTH_URL}/userinfo`, { + headers: { + accessToken: token, + clientId: process.env.AUTH_CLIENT_ID, + }, + }) + .then((response) => response.data); +} + /** * Validate token received in header. * @@ -38,10 +56,10 @@ const getTokenFromHeaders = req => { * @param {Object} res * @param {Object} next */ -export async function authenticateUser(req, res, next) { +async function authenticateUser(req, res, next) { try { const token = getTokenFromHeaders(req); - const user = await userServices.fetchUserByToken(token); + const user = await fetchUserByToken(token); req.token = token; req.currentUser = user.data; @@ -53,7 +71,7 @@ export async function authenticateUser(req, res, next) { return next( new NetworkError({ message: INTERNAL_ERROR, - code: HttpStatus.INTERNAL_SERVER_ERROR + code: HttpStatus.INTERNAL_SERVER_ERROR, }) ); } @@ -65,8 +83,10 @@ export async function authenticateUser(req, res, next) { return next( new TokenError({ code: 401, - message: 'Not Authorized' + message: 'Not Authorized', }) ); } -} \ No newline at end of file +} + +export default authenticateUser; diff --git a/src/routes.js b/src/routes.js index 0e997d4..f6601a1 100644 --- a/src/routes.js +++ b/src/routes.js @@ -1,5 +1,7 @@ import { Router } from 'express'; +import authenticateUser from './auth'; + import swaggerSpec from './utils/swagger'; import userRoutes from './routes/userRoutes'; @@ -15,13 +17,18 @@ router.get('/swagger.json', (req, res) => { res.json(swaggerSpec); }); +/** + * LMS Authentication middleware + */ +router.use(authenticateUser); + /** * GET /api */ router.get('/', (req, res) => { res.json({ app: req.app.locals.title, - apiVersion: req.app.locals.version + apiVersion: req.app.locals.version, }); }); diff --git a/src/utils/http.js b/src/utils/http.js new file mode 100644 index 0000000..b12d7cc --- /dev/null +++ b/src/utils/http.js @@ -0,0 +1,28 @@ +import axios from 'axios'; +import HttpStatus from 'http-status-codes'; + +import NetworkError from '../errors/network'; + +/** + * Axios Object + */ +const http = axios.create(); + +http.interceptors.request.use( + (config) => { + return config; + }, + function (error) { + if (error.response) { + return Promise.reject(error); + } else { + return Promise.reject( + new NetworkError({ + code: HttpStatus.INTERNAL_SERVER_ERROR, + }) + ); + } + } +); + +export { http }; diff --git a/yarn.lock b/yarn.lock index 3900fdf..939c499 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1197,6 +1197,13 @@ atob@^2.1.2: resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg== +axios@^0.19.2: + version "0.19.2" + resolved "https://registry.yarnpkg.com/axios/-/axios-0.19.2.tgz#3ea36c5d8818d0d5f8a8a97a6d36b86cdc00cb27" + integrity sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA== + dependencies: + follow-redirects "1.5.10" + babel-plugin-dynamic-import-node@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.0.tgz#f00f507bdaa3c3e3ff6e7e5e98d90a7acab96f7f" @@ -1943,6 +1950,13 @@ debug@4, debug@4.1.1, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1: dependencies: ms "^2.1.1" +debug@=3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" + integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g== + dependencies: + ms "2.0.0" + decamelize@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" @@ -2662,6 +2676,13 @@ flatted@^2.0.0: resolved "https://registry.yarnpkg.com/flatted/-/flatted-2.0.1.tgz#69e57caa8f0eacbc281d2e2cb458d46fdb449e08" integrity sha512-a1hQMktqW9Nmqr5aktAux3JMNqaucxGcjtjWnZLHX7yyPCmlSV3M54nGYbqT8K+0GhF3NBgmJCc3ma+WOgX8Jg== +follow-redirects@1.5.10: + version "1.5.10" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.5.10.tgz#7b7a9f9aea2fdff36786a94ff643ed07f4ff5e2a" + integrity sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ== + dependencies: + debug "=3.1.0" + for-in@^1.0.1, for-in@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80" From d6febc9a1362d040d9114e92f0d99d2b021768a2 Mon Sep 17 00:00:00 2001 From: Robus Gauli Date: Wed, 8 Apr 2020 18:24:05 +0545 Subject: [PATCH 04/24] Continuation of user session for req-resp roundtrip --- README.md | 1 - package.json | 2 + src/auth/index.js | 38 ++++++++----------- src/auth/session.js | 14 +++++++ src/constants/db.js | 1 + src/controllers/users.js | 58 +++++----------------------- src/db.js | 6 +-- src/errors/database.js | 27 +++++++++++++ src/errors/error.js | 10 ++--- src/errors/network.js | 16 +++++--- src/errors/token.js | 15 ++++++-- src/index.js | 4 +- src/knexfile.js | 10 ++--- src/middlewares/errorHandler.js | 12 +++--- src/models/model.js | 46 ++++++++++++++++++++++ src/models/user.js | 27 +++++-------- src/routes/userRoutes.js | 18 +-------- src/seeds/01_insert_users.js | 8 +--- src/services/userService.js | 67 +++++++++------------------------ src/utils/buildError.js | 10 ++--- src/utils/logger.js | 10 ++--- src/utils/swagger.js | 8 ++-- src/validators/userValidator.js | 9 ++--- test/api.test.js | 8 ++-- test/controllers/users.test.js | 30 +++++++-------- yarn.lock | 42 ++++++++++++++++++++- 26 files changed, 263 insertions(+), 234 deletions(-) create mode 100644 src/auth/session.js create mode 100644 src/constants/db.js create mode 100644 src/errors/database.js create mode 100644 src/models/model.js diff --git a/README.md b/README.md index 90a9d85..00018c2 100644 --- a/README.md +++ b/README.md @@ -100,4 +100,3 @@ To run the tests you need to create a separate test database. Don't forget to up Run tests with coverage. $ yarn test:coverage - diff --git a/package.json b/package.json index cdc2da6..bb107e3 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,9 @@ "body-parser": "^1.19.0", "bookshelf": "^1.1.0", "bookshelf-virtuals-plugin": "^0.1.1", + "boom": "^7.3.0", "compression": "^1.7.4", + "continuation-local-storage": "^3.2.1", "cors": "^2.8.5", "dotenv": "^8.2.0", "express": "^4.17.1", diff --git a/src/auth/index.js b/src/auth/index.js index dff6bd3..95c569f 100644 --- a/src/auth/index.js +++ b/src/auth/index.js @@ -6,7 +6,8 @@ import NetworkError from '../errors/network'; import { http } from '../utils/http'; import logger from '../utils/logger'; -const EMPTY_TOKEN = 'Access token not provided.'; +import { userSession } from './session'; + const INTERNAL_ERROR = 'Internal Error'; /** @@ -24,10 +25,8 @@ const getTokenFromHeaders = (req) => { return authorization.split(' ')[1]; } } - logger.error(`Token Error: ${EMPTY_TOKEN}`); throw new TokenError({ - message: EMPTY_TOKEN, code: HttpStatus.UNAUTHORIZED, }); }; @@ -58,16 +57,21 @@ export function fetchUserByToken(token) { */ async function authenticateUser(req, res, next) { try { - const token = getTokenFromHeaders(req); - const user = await fetchUserByToken(token); - - req.token = token; - req.currentUser = user.data; - next(); + userSession.run(() => { + const token = "random token" + const user = { + "name" :"random user" + } + userSession.set('user', { + ...user, + }); + next(); + + }) + } catch (error) { + logger.error(error); if (error instanceof NetworkError) { - logger.error(`Network Error: ${error}`); - return next( new NetworkError({ message: INTERNAL_ERROR, @@ -75,17 +79,7 @@ async function authenticateUser(req, res, next) { }) ); } - - if (error instanceof TokenError) { - return next(error); - } - - return next( - new TokenError({ - code: 401, - message: 'Not Authorized', - }) - ); + next(error); } } diff --git a/src/auth/session.js b/src/auth/session.js new file mode 100644 index 0000000..62ddb13 --- /dev/null +++ b/src/auth/session.js @@ -0,0 +1,14 @@ +import { createNamespace, getNamespace } from 'continuation-local-storage'; + +/** + * User session namespace limited to request response roundtrip. + */ +export const userSession = createNamespace('user-namespace'); + +/** + * User object that is consumed by services. + */ +const session = getNamespace('user-namespace'); +const user = () => session.get('user'); + +export default user; diff --git a/src/constants/db.js b/src/constants/db.js new file mode 100644 index 0000000..8be10c0 --- /dev/null +++ b/src/constants/db.js @@ -0,0 +1 @@ +export const DB_DUPLICATE_ENTRY = 'ER_DUP_ENTRY'; \ No newline at end of file diff --git a/src/controllers/users.js b/src/controllers/users.js index 65fdb40..a77a55d 100644 --- a/src/controllers/users.js +++ b/src/controllers/users.js @@ -1,6 +1,7 @@ import HttpStatus from 'http-status-codes'; import * as userService from '../services/userService'; +import logger from '../utils/logger'; /** * Get all users. @@ -12,22 +13,8 @@ import * as userService from '../services/userService'; export function fetchAll(req, res, next) { userService .getAllUsers() - .then(data => res.json({ data })) - .catch(err => next(err)); -} - -/** - * Get a user by its id. - * - * @param {Object} req - * @param {Object} res - * @param {Function} next - */ -export function fetchById(req, res, next) { - userService - .getUser(req.params.id) - .then(data => res.json({ data })) - .catch(err => next(err)); + .then((data) => res.json({ data })) + .catch((err) => next(err)); } /** @@ -38,36 +25,11 @@ export function fetchById(req, res, next) { * @param {Function} next */ export function create(req, res, next) { - userService - .createUser(req.body) - .then(data => res.status(HttpStatus.CREATED).json({ data })) - .catch(err => next(err)); -} - -/** - * Update a user. - * - * @param {Object} req - * @param {Object} res - * @param {Function} next - */ -export function update(req, res, next) { - userService - .updateUser(req.params.id, req.body) - .then(data => res.json({ data })) - .catch(err => next(err)); -} - -/** - * Delete a user. - * - * @param {Object} req - * @param {Object} res - * @param {Function} next - */ -export function deleteUser(req, res, next) { - userService - .deleteUser(req.params.id) - .then(data => res.status(HttpStatus.NO_CONTENT).json({ data })) - .catch(err => next(err)); + try { + const data = userService.createUser(req.body); + res.status(HttpStatus.CREATED).json({data}) + } catch(err) { + logger.error(err); + next(err) + } } diff --git a/src/db.js b/src/db.js index ea3279b..ed8592f 100644 --- a/src/db.js +++ b/src/db.js @@ -1,5 +1,4 @@ import knexJs from 'knex'; -import bookshelfJs from 'bookshelf'; import knexConfig from './knexfile'; @@ -7,8 +6,5 @@ import knexConfig from './knexfile'; * Database connection. */ const knex = knexJs(knexConfig); -const bookshelf = bookshelfJs(knex); -bookshelf.plugin(['bookshelf-virtuals-plugin']); - -export default bookshelf; +export default knex; diff --git a/src/errors/database.js b/src/errors/database.js new file mode 100644 index 0000000..603e609 --- /dev/null +++ b/src/errors/database.js @@ -0,0 +1,27 @@ +import BaseError from './error'; + +/** + * Error class for database failure and error. + */ +class DatabaseError extends BaseError { + /** + * Constructor of DatabaseError. + * + * @param {Object} error + * @param {String} error.title + * @param {String} error.message + * @param {Number} error.code + */ + constructor({ title = '', message = '', code = 500 }) { + super(message); + this.title = title; + this.message = message; + this.code = code; + } + + toString() { + return `${this.title}[${this.code}]`; + } +} + +export default DatabaseError; \ No newline at end of file diff --git a/src/errors/error.js b/src/errors/error.js index 4fd43da..2ac4ada 100644 --- a/src/errors/error.js +++ b/src/errors/error.js @@ -1,10 +1,10 @@ /** * Base class for error. */ -class BaseClass extends Error { - constructor(message) { - super(message); - } +class BaseError extends Error { + constructor(message) { + super(message); + } } -export default BaseClass \ No newline at end of file +export default BaseError; diff --git a/src/errors/network.js b/src/errors/network.js index 472b640..3ca8a95 100644 --- a/src/errors/network.js +++ b/src/errors/network.js @@ -1,6 +1,7 @@ - import BaseError from './error'; +const TITLE = 'Network error'; + /** * Error class for Network error. */ @@ -9,15 +10,20 @@ class NetworkError extends BaseError { * Constructor of NetworkError. * * @param {Object} error + * @param {String} error.title * @param {String} error.message - * @param {String} error.detail * @param {Number} error.code */ - constructor({ message = '', detail = '', code = 500 }) { + constructor({ title = TITLE, message = '', code = 500 }) { super(message); - this.detail = detail; + this.title = title; + this.message = message; this.code = code; } + + toString() { + return `${this.title}[${this.code}]`; + } } -export default NetworkError; \ No newline at end of file +export default NetworkError; diff --git a/src/errors/token.js b/src/errors/token.js index 433dfd8..7d76722 100644 --- a/src/errors/token.js +++ b/src/errors/token.js @@ -1,5 +1,7 @@ import BaseError from './error'; +const TITLE = 'Invalid access token'; + /** * Error class for Token Error. */ @@ -8,15 +10,20 @@ class TokenError extends BaseError { * Constructor of NetworkError. * * @param {Object} error + * @param {String} error.title * @param {String} error.message - * @param {String} error.details * @param {Number} error.code */ - constructor({ message = '', detail = '', code = 401 }) { + constructor({ title = TITLE, message = '', code = 401 }) { super(message); - this.detail = detail; + this.title = title; + this.message = message; this.code = code; } + + toString() { + return `${this.title} [${this.code}]`; + } } -export default TokenError; \ No newline at end of file +export default TokenError; diff --git a/src/index.js b/src/index.js index f47e567..e3f7ce2 100644 --- a/src/index.js +++ b/src/index.js @@ -74,7 +74,7 @@ app.listen(app.get('port'), app.get('host'), () => { }); // Catch unhandled rejections -process.on('unhandledRejection', err => { +process.on('unhandledRejection', (err) => { logger.error('Unhandled rejection', err); try { @@ -87,7 +87,7 @@ process.on('unhandledRejection', err => { }); // Catch uncaught exceptions -process.on('uncaughtException', err => { +process.on('uncaughtException', (err) => { logger.error('Uncaught exception', err); try { diff --git a/src/knexfile.js b/src/knexfile.js index 489572e..b4cec0b 100644 --- a/src/knexfile.js +++ b/src/knexfile.js @@ -8,7 +8,7 @@ let connection = { password: process.env.DB_PASSWORD, database: process.env.DB_NAME, charset: 'utf8', - timezone: 'UTC' + timezone: 'UTC', }; // For test environment @@ -19,7 +19,7 @@ if (process.env.NODE_ENV === 'test') { host: process.env.TEST_DB_HOST, user: process.env.TEST_DB_USER, password: process.env.TEST_DB_PASSWORD, - database: process.env.TEST_DB_NAME + database: process.env.TEST_DB_NAME, }; } @@ -32,10 +32,10 @@ module.exports = { migrations: { tableName: 'migrations', directory: './migrations', - stub: './stubs/migration.stub' + stub: './stubs/migration.stub', }, seeds: { directory: './seeds', - stub: './stubs/seed.stub' - } + stub: './stubs/seed.stub', + }, }; diff --git a/src/middlewares/errorHandler.js b/src/middlewares/errorHandler.js index 121a4b6..a348359 100644 --- a/src/middlewares/errorHandler.js +++ b/src/middlewares/errorHandler.js @@ -13,8 +13,8 @@ export function notFound(req, res) { res.status(HttpStatus.NOT_FOUND).json({ error: { code: HttpStatus.NOT_FOUND, - message: HttpStatus.getStatusText(HttpStatus.NOT_FOUND) - } + message: HttpStatus.getStatusText(HttpStatus.NOT_FOUND), + }, }); } @@ -29,8 +29,8 @@ export function methodNotAllowed(req, res) { res.status(HttpStatus.METHOD_NOT_ALLOWED).json({ error: { code: HttpStatus.METHOD_NOT_ALLOWED, - message: HttpStatus.getStatusText(HttpStatus.METHOD_NOT_ALLOWED) - } + message: HttpStatus.getStatusText(HttpStatus.METHOD_NOT_ALLOWED), + }, }); } @@ -49,8 +49,8 @@ export function bodyParser(err, req, res, next) { res.status(err.status).json({ error: { code: err.status, - message: HttpStatus.getStatusText(err.status) - } + message: HttpStatus.getStatusText(err.status), + }, }); } diff --git a/src/models/model.js b/src/models/model.js new file mode 100644 index 0000000..148cc49 --- /dev/null +++ b/src/models/model.js @@ -0,0 +1,46 @@ +import db from '../db'; + +/** + * Base class that is extended by domain models such as users, leave, etc. + */ +class Model { + constructor(dbname) { + this._db = db(dbname); + } + + save(payload = {}) { + return new Promise((resolve, reject) => { + db.transaction((trx) => { + this._db + .transacting(trx) + .insert(payload) + .then((response) => { + trx.commit(response); + }) + .catch((err) => { + trx.rollback(err); + }); + }) + .then((response) => { + resolve(response); + }) + .catch((err) => { + reject(err); + }); + }); + } + + fetchAll() { + return this._db.select('*'); + } + + fetchBy(where) { + return this._db.where(where).first(); + } + + fetchAllBy(where) { + return this._db.where(where); + } +} + +export default Model; \ No newline at end of file diff --git a/src/models/user.js b/src/models/user.js index d032e7d..c9a32d7 100644 --- a/src/models/user.js +++ b/src/models/user.js @@ -1,24 +1,15 @@ -import bookshelf from '../db'; +import Model from './model'; -const TABLE_NAME = 'users'; /** - * User model. + * User model for basic CRUD. */ -class User extends bookshelf.Model { - /** - * Get table name. - */ - get tableName() { - return TABLE_NAME; - } - - /** - * Table has timestamps. - */ - get hasTimestamps() { - return true; - } +class User extends Model { + constructor() { + super(User.Table); + } } -export default User; +User.Table = "users"; + +export default User; \ No newline at end of file diff --git a/src/routes/userRoutes.js b/src/routes/userRoutes.js index eb60806..34f7d3e 100644 --- a/src/routes/userRoutes.js +++ b/src/routes/userRoutes.js @@ -1,7 +1,6 @@ import { Router } from 'express'; import * as userController from '../controllers/users'; -import { findUser, userValidator } from '../validators/userValidator'; const router = Router(); @@ -10,24 +9,9 @@ const router = Router(); */ router.get('/', userController.fetchAll); -/** - * GET /api/users/:id - */ -router.get('/:id', userController.fetchById); - /** * POST /api/users */ -router.post('/', userValidator, userController.create); - -/** - * PUT /api/users/:id - */ -router.put('/:id', findUser, userValidator, userController.update); - -/** - * DELETE /api/users/:id - */ -router.delete('/:id', findUser, userController.deleteUser); +router.post('/', userController.create); export default router; diff --git a/src/seeds/01_insert_users.js b/src/seeds/01_insert_users.js index ea49b1e..32ee6fe 100644 --- a/src/seeds/01_insert_users.js +++ b/src/seeds/01_insert_users.js @@ -10,13 +10,9 @@ export function seed(knex) { .then(() => { return knex('users').insert([ { - name: 'Saugat Acharya', - updated_at: new Date() + name: 'Sample user', + updated_at: new Date(), }, - { - name: 'John Doe', - updated_at: new Date() - } ]); }); } diff --git a/src/services/userService.js b/src/services/userService.js index 76440ec..0eebf54 100644 --- a/src/services/userService.js +++ b/src/services/userService.js @@ -1,58 +1,27 @@ -import Boom from '@hapi/boom'; - import User from '../models/user'; +import userSession from '../auth/session' -/** - * Get all users. - * - * @returns {Promise} - */ -export function getAllUsers() { - return User.fetchAll(); -} - -/** - * Get a user. - * - * @param {Number|String} id - * @returns {Promise} - */ -export function getUser(id) { - return new User({ id }) - .fetch() - .then(user => user) - .catch(User.NotFoundError, () => { - throw Boom.notFound('User not found'); - }); -} +import logger from '../utils/logger'; /** - * Create new user. - * - * @param {Object} user - * @returns {Promise} - */ -export function createUser(user) { - return new User({ name: user.name }).save(); -} - -/** - * Update a user. + * Get all users. * - * @param {Number|String} id - * @param {Object} user * @returns {Promise} */ -export function updateUser(id, user) { - return new User({ id }).save({ name: user.name }); +export async function getAllUsers() { + const users = await new User().fetchAll(); + return users; } -/** - * Delete a user. - * - * @param {Number|String} id - * @returns {Promise} - */ -export function deleteUser(id) { - return new User({ id }).fetch().then(user => user.destroy()); -} +export async function createUser() { + const user = userSession(); + try { + const id = await new User().save(user); + logger.info(`User created: ${user}`) + return { + id: id.pop(), + }; + } catch (err) { + throw err; + } +} \ No newline at end of file diff --git a/src/utils/buildError.js b/src/utils/buildError.js index 9a37bce..ef32b04 100644 --- a/src/utils/buildError.js +++ b/src/utils/buildError.js @@ -14,12 +14,12 @@ function buildError(err) { message: HttpStatus.getStatusText(HttpStatus.BAD_REQUEST), details: err.details && - err.details.map(err => { + err.details.map((err) => { return { message: err.message, - param: err.path.join('.') + param: err.path.join('.'), }; - }) + }), }; } @@ -27,14 +27,14 @@ function buildError(err) { if (err.isBoom) { return { code: err.output.statusCode, - message: err.output.payload.message || err.output.payload.error + message: err.output.payload.message || err.output.payload.error, }; } // Return INTERNAL_SERVER_ERROR for all other cases return { code: HttpStatus.INTERNAL_SERVER_ERROR, - message: HttpStatus.getStatusText(HttpStatus.INTERNAL_SERVER_ERROR) + message: HttpStatus.getStatusText(HttpStatus.INTERNAL_SERVER_ERROR), }; } diff --git a/src/utils/logger.js b/src/utils/logger.js index a64858e..edb51fa 100644 --- a/src/utils/logger.js +++ b/src/utils/logger.js @@ -19,7 +19,7 @@ const logger = winston.createLogger({ transports: [ new winston.transports.Console({ format: format.combine(format.colorize(), format.simple()), - level: 'info' + level: 'info', }), new winston.transports.DailyRotateFile({ format: format.combine(format.timestamp(), format.json()), @@ -27,9 +27,9 @@ const logger = winston.createLogger({ level: LOG_LEVEL, dirname: LOG_DIR, datePattern: 'YYYY-MM-DD', - filename: '%DATE%-debug.log' - }) - ] + filename: '%DATE%-debug.log', + }), + ], }); export const logStream = { @@ -40,7 +40,7 @@ export const logStream = { */ write(message) { logger.info(message.toString()); - } + }, }; export default logger; diff --git a/src/utils/swagger.js b/src/utils/swagger.js index f44b025..9ca28d7 100644 --- a/src/utils/swagger.js +++ b/src/utils/swagger.js @@ -8,9 +8,9 @@ const swaggerDefinition = { info: { title: process.env.APP_NAME, version: process.env.APP_VERSION, - description: process.env.APP_DESCRIPTION + description: process.env.APP_DESCRIPTION, }, - basePath: '/api' + basePath: '/api', }; /** @@ -24,8 +24,8 @@ const swaggerOptions = { path.join(__dirname, '/../routes.js'), path.join(__dirname, '/../docs/*.js'), path.join(__dirname, '/../docs/*.yml'), - path.join(__dirname, '/../docs/*.yaml') - ] + path.join(__dirname, '/../docs/*.yaml'), + ], }; /** diff --git a/src/validators/userValidator.js b/src/validators/userValidator.js index 2295ffd..b8fa3c8 100644 --- a/src/validators/userValidator.js +++ b/src/validators/userValidator.js @@ -5,10 +5,7 @@ import * as userService from '../services/userService'; // Validation schema const schema = Joi.object({ - name: Joi.string() - .label('Name') - .max(90) - .required() + name: Joi.string().label('Name').max(90).required(), }); /** @@ -22,7 +19,7 @@ const schema = Joi.object({ function userValidator(req, res, next) { return validate(req.body, schema) .then(() => next()) - .catch(err => next(err)); + .catch((err) => next(err)); } /** @@ -37,7 +34,7 @@ function findUser(req, res, next) { return userService .getUser(req.params.id) .then(() => next()) - .catch(err => next(err)); + .catch((err) => next(err)); } export { findUser, userValidator }; diff --git a/test/api.test.js b/test/api.test.js index 3524d00..86674a8 100644 --- a/test/api.test.js +++ b/test/api.test.js @@ -3,7 +3,7 @@ import app from '../src/index'; import request from 'supertest'; describe('Base API Test', () => { - it('should return API version and title for the app', done => { + it('should return API version and title for the app', (done) => { request(app) .get('/api') .end((err, res) => { @@ -15,10 +15,8 @@ describe('Base API Test', () => { }); }); - it('should return 405 method not allowed for random API hits', done => { - const randomString = Math.random() - .toString(36) - .substr(2, 5); + it('should return 405 method not allowed for random API hits', (done) => { + const randomString = Math.random().toString(36).substr(2, 5); request(app) .get(`/api/${randomString}`) diff --git a/test/controllers/users.test.js b/test/controllers/users.test.js index 30ad275..5a6c9f7 100644 --- a/test/controllers/users.test.js +++ b/test/controllers/users.test.js @@ -7,14 +7,14 @@ import bookshelf from '../../src/db'; * Tests for '/api/users' */ describe('Users Controller Test', () => { - before(done => { + before((done) => { bookshelf .knex('users') .truncate() .then(() => done()); }); - it('should return list of users', done => { + it('should return list of users', (done) => { request(app) .get('/api/users') .end((err, res) => { @@ -26,9 +26,9 @@ describe('Users Controller Test', () => { }); }); - it('should not create a new user if name is not provided', done => { + it('should not create a new user if name is not provided', (done) => { const user = { - noname: 'Jane Doe' + noname: 'Jane Doe', }; request(app) @@ -48,9 +48,9 @@ describe('Users Controller Test', () => { }); }); - it('should create a new user with valid data', done => { + it('should create a new user with valid data', (done) => { const user = { - name: 'Jane Doe' + name: 'Jane Doe', }; request(app) @@ -71,7 +71,7 @@ describe('Users Controller Test', () => { }); }); - it('should get information of user', done => { + it('should get information of user', (done) => { request(app) .get('/api/users/1') .end((err, res) => { @@ -88,7 +88,7 @@ describe('Users Controller Test', () => { }); }); - it('should respond with not found error if random user id is provided', done => { + it('should respond with not found error if random user id is provided', (done) => { request(app) .get('/api/users/1991') .end((err, res) => { @@ -102,9 +102,9 @@ describe('Users Controller Test', () => { }); }); - it('should update a user if name is provided', done => { + it('should update a user if name is provided', (done) => { const user = { - name: 'John Doe' + name: 'John Doe', }; request(app) @@ -124,9 +124,9 @@ describe('Users Controller Test', () => { }); }); - it('should not update a user if name is not provided', done => { + it('should not update a user if name is not provided', (done) => { const user = { - noname: 'John Doe' + noname: 'John Doe', }; request(app) @@ -146,7 +146,7 @@ describe('Users Controller Test', () => { }); }); - it('should delete a user if valid id is provided', done => { + it('should delete a user if valid id is provided', (done) => { request(app) .delete('/api/users/1') .end((err, res) => { @@ -156,7 +156,7 @@ describe('Users Controller Test', () => { }); }); - it('should respond with not found error if random user id is provided for deletion', done => { + it('should respond with not found error if random user id is provided for deletion', (done) => { request(app) .delete('/api/users/1991') .end((err, res) => { @@ -170,7 +170,7 @@ describe('Users Controller Test', () => { }); }); - it('should respond with bad request for empty JSON in request body', done => { + it('should respond with bad request for empty JSON in request body', (done) => { const user = {}; request(app) diff --git a/yarn.lock b/yarn.lock index 939c499..cdec250 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1180,6 +1180,14 @@ async-each@^1.0.1: resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.3.tgz#b727dbf87d7651602f06f4d4ac387f47d91b0cbf" integrity sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ== +async-listener@^0.6.0: + version "0.6.10" + resolved "https://registry.yarnpkg.com/async-listener/-/async-listener-0.6.10.tgz#a7c97abe570ba602d782273c0de60a51e3e17cbc" + integrity sha512-gpuo6xOyF4D5DE5WvyqZdPA3NGhiT6Qf07l7DCB0wwDEsLvDIbCr6j9S5aj5Ch96dLace5tXVzWBZkxU/c5ohw== + dependencies: + semver "^5.3.0" + shimmer "^1.1.0" + async@^2.6.1: version "2.6.3" resolved "https://registry.yarnpkg.com/async/-/async-2.6.3.tgz#d72625e2344a3656e3a3ad4fa749fa83299d82ff" @@ -1296,6 +1304,13 @@ bookshelf@^1.1.0: inflection "^1.12.0" lodash "^4.17.15" +boom@^7.3.0: + version "7.3.0" + resolved "https://registry.yarnpkg.com/boom/-/boom-7.3.0.tgz#733a6d956d33b0b1999da3fe6c12996950d017b9" + integrity sha512-Swpoyi2t5+GhOEGw8rEsKvTxFLIDiiKoUc2gsoV6Lyr43LHBIzch3k2MvYUs8RTROrIkVJ3Al0TkaOGjnb+B6A== + dependencies: + hoek "6.x.x" + bowser@2.9.0: version "2.9.0" resolved "https://registry.yarnpkg.com/bowser/-/bowser-2.9.0.tgz#3bed854233b419b9a7422d9ee3e85504373821c9" @@ -1792,6 +1807,14 @@ content-type@~1.0.4: resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA== +continuation-local-storage@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/continuation-local-storage/-/continuation-local-storage-3.2.1.tgz#11f613f74e914fe9b34c92ad2d28fe6ae1db7ffb" + integrity sha512-jx44cconVqkCEEyLSKWwkvUXwO561jXMa3LPjTPsm5QR22PA0/mhe33FT4Xb5y74JDvt/Cq+5lm8S8rskLv9ZA== + dependencies: + async-listener "^0.6.0" + emitter-listener "^1.1.1" + convert-source-map@^1.1.0, convert-source-map@^1.7.0: version "1.7.0" resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.7.0.tgz#17a2cb882d7f77d3490585e2ce6c524424a3a442" @@ -2120,6 +2143,13 @@ elegant-spinner@^1.0.1: resolved "https://registry.yarnpkg.com/elegant-spinner/-/elegant-spinner-1.0.1.tgz#db043521c95d7e303fd8f345bedc3349cfb0729e" integrity sha1-2wQ1IcldfjA/2PNFvtwzSc+wcp4= +emitter-listener@^1.1.1: + version "1.1.2" + resolved "https://registry.yarnpkg.com/emitter-listener/-/emitter-listener-1.1.2.tgz#56b140e8f6992375b3d7cb2cab1cc7432d9632e8" + integrity sha512-Bt1sBAGFHY9DKY+4/2cV6izcKJUf5T7/gkdmkxzX/qv9CcGH8xSwVRW5mtX03SWJtRTWSOpzCuWN9rBFYZepZQ== + dependencies: + shimmer "^1.2.0" + emoji-regex@^7.0.1: version "7.0.3" resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156" @@ -3043,6 +3073,11 @@ hide-powered-by@1.1.0: resolved "https://registry.yarnpkg.com/hide-powered-by/-/hide-powered-by-1.1.0.tgz#be3ea9cab4bdb16f8744be873755ca663383fa7a" integrity sha512-Io1zA2yOA1YJslkr+AJlWSf2yWFkKjvkcL9Ni1XSUqnGLr/qRQe2UI3Cn/J9MsJht7yEVCe0SscY1HgVMujbgg== +hoek@6.x.x: + version "6.1.3" + resolved "https://registry.yarnpkg.com/hoek/-/hoek-6.1.3.tgz#73b7d33952e01fe27a38b0457294b79dd8da242c" + integrity sha512-YXXAAhmF9zpQbC7LEcREFtXfGq5K1fmd+4PHkBq8NUqmzW3G+Dq10bI/i0KucLRwss3YYFQ0fSfoxBZYiGUqtQ== + homedir-polyfill@^1.0.1: version "1.0.3" resolved "https://registry.yarnpkg.com/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz#743298cef4e5af3e194161fbadcc2151d3a058e8" @@ -5171,7 +5206,7 @@ semver@7.0.0: resolved "https://registry.yarnpkg.com/semver/-/semver-7.0.0.tgz#5f3ca35761e47e05b206c6daff2cf814f0316b8e" integrity sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A== -semver@^5.0.3, semver@^5.1.0, semver@^5.4.1, semver@^5.5.0, semver@^5.6.0, semver@^5.7.0, semver@^5.7.1: +semver@^5.0.3, semver@^5.1.0, semver@^5.3.0, semver@^5.4.1, semver@^5.5.0, semver@^5.6.0, semver@^5.7.0, semver@^5.7.1: version "5.7.1" resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== @@ -5265,6 +5300,11 @@ shebang-regex@^3.0.0: resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== +shimmer@^1.1.0, shimmer@^1.2.0: + version "1.2.1" + resolved "https://registry.yarnpkg.com/shimmer/-/shimmer-1.2.1.tgz#610859f7de327b587efebf501fb43117f9aff337" + integrity sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw== + side-channel@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.2.tgz#df5d1abadb4e4bf4af1cd8852bf132d2f7876947" From eb71aa658a4f7daada2146664b01c8d6936c5350 Mon Sep 17 00:00:00 2001 From: Robus Gauli Date: Thu, 9 Apr 2020 11:39:13 +0545 Subject: [PATCH 05/24] Add JSDOC for model class --- src/auth/index.js | 25 +++++++-------- src/constants/db.js | 2 +- src/controllers/{users.js => user.js} | 16 +++++----- src/errors/database.js | 15 +++++++-- src/errors/error.js | 6 ++++ src/errors/network.js | 13 ++++++-- src/errors/token.js | 11 ++++++- src/models/model.js | 40 ++++++++++++++---------- src/models/user.js | 15 +++++---- src/routes.js | 2 +- src/routes/{userRoutes.js => user.js} | 2 +- src/services/{userService.js => user.js} | 25 +++++++++------ 12 files changed, 109 insertions(+), 63 deletions(-) rename src/controllers/{users.js => user.js} (66%) rename src/routes/{userRoutes.js => user.js} (79%) rename src/services/{userService.js => user.js} (55%) diff --git a/src/auth/index.js b/src/auth/index.js index 95c569f..41af31c 100644 --- a/src/auth/index.js +++ b/src/auth/index.js @@ -55,23 +55,22 @@ export function fetchUserByToken(token) { * @param {Object} res * @param {Object} next */ -async function authenticateUser(req, res, next) { +function authenticateUser(req, res, next) { try { - userSession.run(() => { - const token = "random token" - const user = { - "name" :"random user" - } + userSession.run(async () => { + const token = getTokenFromHeaders(req); + const user = await fetchUserByToken(token); + userSession.set('user', { ...user, + token, }); + next(); - - }) - - } catch (error) { - logger.error(error); - if (error instanceof NetworkError) { + }); + } catch (err) { + logger.error(err); + if (err instanceof NetworkError) { return next( new NetworkError({ message: INTERNAL_ERROR, @@ -79,7 +78,7 @@ async function authenticateUser(req, res, next) { }) ); } - next(error); + next(err); } } diff --git a/src/constants/db.js b/src/constants/db.js index 8be10c0..b4336e9 100644 --- a/src/constants/db.js +++ b/src/constants/db.js @@ -1 +1 @@ -export const DB_DUPLICATE_ENTRY = 'ER_DUP_ENTRY'; \ No newline at end of file +export const DB_DUPLICATE_ENTRY = 'ER_DUP_ENTRY'; diff --git a/src/controllers/users.js b/src/controllers/user.js similarity index 66% rename from src/controllers/users.js rename to src/controllers/user.js index a77a55d..af5e977 100644 --- a/src/controllers/users.js +++ b/src/controllers/user.js @@ -1,8 +1,9 @@ import HttpStatus from 'http-status-codes'; -import * as userService from '../services/userService'; import logger from '../utils/logger'; +import * as userService from '../services/user'; + /** * Get all users. * @@ -12,9 +13,9 @@ import logger from '../utils/logger'; */ export function fetchAll(req, res, next) { userService - .getAllUsers() + .fetchAll() .then((data) => res.json({ data })) - .catch((err) => next(err)); + .catch(next); } /** @@ -26,10 +27,11 @@ export function fetchAll(req, res, next) { */ export function create(req, res, next) { try { - const data = userService.createUser(req.body); - res.status(HttpStatus.CREATED).json({data}) - } catch(err) { + const data = userService.create(req.body); + + res.status(HttpStatus.CREATED).json({ data }); + } catch (err) { logger.error(err); - next(err) + next(err); } } diff --git a/src/errors/database.js b/src/errors/database.js index 603e609..76ce5d7 100644 --- a/src/errors/database.js +++ b/src/errors/database.js @@ -1,9 +1,13 @@ import BaseError from './error'; /** + * @class DatabaseError + * @extends BaseError + * * Error class for database failure and error. */ class DatabaseError extends BaseError { + /** * Constructor of DatabaseError. * @@ -18,10 +22,17 @@ class DatabaseError extends BaseError { this.message = message; this.code = code; } - + + /** + * Returns the formatted string representation of error. + * + * @method DatabaseError#toString + * + * @returns {String} + */ toString() { return `${this.title}[${this.code}]`; } } -export default DatabaseError; \ No newline at end of file +export default DatabaseError; diff --git a/src/errors/error.js b/src/errors/error.js index 2ac4ada..3d38fdc 100644 --- a/src/errors/error.js +++ b/src/errors/error.js @@ -2,6 +2,12 @@ * Base class for error. */ class BaseError extends Error { + + /** + * Constructor method for BaseError. + * + * @returns {BaseError} + */ constructor(message) { super(message); } diff --git a/src/errors/network.js b/src/errors/network.js index 3ca8a95..b4ef9f8 100644 --- a/src/errors/network.js +++ b/src/errors/network.js @@ -7,12 +7,14 @@ const TITLE = 'Network error'; */ class NetworkError extends BaseError { /** - * Constructor of NetworkError. + * Constructor for NetworkError. * * @param {Object} error * @param {String} error.title * @param {String} error.message * @param {Number} error.code + * + * @returns {NetworkError} */ constructor({ title = TITLE, message = '', code = 500 }) { super(message); @@ -20,7 +22,14 @@ class NetworkError extends BaseError { this.message = message; this.code = code; } - + + /** + * Returns the formatted string representation of error. + * + * @method NetworkError#toString + * + * @returns {String} + */ toString() { return `${this.title}[${this.code}]`; } diff --git a/src/errors/token.js b/src/errors/token.js index 7d76722..1240f2c 100644 --- a/src/errors/token.js +++ b/src/errors/token.js @@ -7,12 +7,14 @@ const TITLE = 'Invalid access token'; */ class TokenError extends BaseError { /** - * Constructor of NetworkError. + * Constructor of TokenError. * * @param {Object} error * @param {String} error.title * @param {String} error.message * @param {Number} error.code + * + * @returns {TokenError} */ constructor({ title = TITLE, message = '', code = 401 }) { super(message); @@ -21,6 +23,13 @@ class TokenError extends BaseError { this.code = code; } + /** + * Returns the formatted string representation of error. + * + * @method TokenError#toString + * + * @returns {String} + */ toString() { return `${this.title} [${this.code}]`; } diff --git a/src/models/model.js b/src/models/model.js index 148cc49..ed9473d 100644 --- a/src/models/model.js +++ b/src/models/model.js @@ -1,17 +1,35 @@ import db from '../db'; /** + * @class Model + * * Base class that is extended by domain models such as users, leave, etc. */ class Model { - constructor(dbname) { - this._db = db(dbname); + constructor() { + this._db = db(this.getTable()); } - + + /** + * This method is required by the domain class. + * + * @returns {String} + */ + getTable() { + throw new Error('Not implemented'); + } + + /** + * This method persists the payload object to underlying database. + * + * @param {Object} payload + * + * @returns {Promise} + */ save(payload = {}) { return new Promise((resolve, reject) => { db.transaction((trx) => { - this._db + this._db .transacting(trx) .insert(payload) .then((response) => { @@ -29,18 +47,6 @@ class Model { }); }); } - - fetchAll() { - return this._db.select('*'); - } - - fetchBy(where) { - return this._db.where(where).first(); - } - - fetchAllBy(where) { - return this._db.where(where); - } } -export default Model; \ No newline at end of file +export default Model; diff --git a/src/models/user.js b/src/models/user.js index c9a32d7..3b301c8 100644 --- a/src/models/user.js +++ b/src/models/user.js @@ -1,15 +1,14 @@ import Model from './model'; - /** - * User model for basic CRUD. + * @class User + * + * User model for basic representing user entity. */ class User extends Model { - constructor() { - super(User.Table); - } + getTable() { + return 'users'; + } } -User.Table = "users"; - -export default User; \ No newline at end of file +export default User; diff --git a/src/routes.js b/src/routes.js index f6601a1..3699b3c 100644 --- a/src/routes.js +++ b/src/routes.js @@ -3,7 +3,7 @@ import { Router } from 'express'; import authenticateUser from './auth'; import swaggerSpec from './utils/swagger'; -import userRoutes from './routes/userRoutes'; +import userRoutes from './routes/user'; /** * Contains all API routes for the application. diff --git a/src/routes/userRoutes.js b/src/routes/user.js similarity index 79% rename from src/routes/userRoutes.js rename to src/routes/user.js index 34f7d3e..bc587c4 100644 --- a/src/routes/userRoutes.js +++ b/src/routes/user.js @@ -1,6 +1,6 @@ import { Router } from 'express'; -import * as userController from '../controllers/users'; +import * as userController from '../controllers/user'; const router = Router(); diff --git a/src/services/userService.js b/src/services/user.js similarity index 55% rename from src/services/userService.js rename to src/services/user.js index 0eebf54..32ed1a1 100644 --- a/src/services/userService.js +++ b/src/services/user.js @@ -1,5 +1,5 @@ import User from '../models/user'; -import userSession from '../auth/session' +import userSession from '../auth/session'; import logger from '../utils/logger'; @@ -8,20 +8,25 @@ import logger from '../utils/logger'; * * @returns {Promise} */ -export async function getAllUsers() { +export async function fetchAll() { const users = await new User().fetchAll(); - return users; + + return users; } -export async function createUser() { +/** + * Create a new user. + * + * @returns {Promise} + */ +export async function create() { const user = userSession(); - try { + const id = await new User().save(user); - logger.info(`User created: ${user}`) + + logger.info(`User created: ${user}`); + return { id: id.pop(), }; - } catch (err) { - throw err; - } -} \ No newline at end of file +} From 3f5c4d0252aa47c0177df62763b3a16bc072aaf9 Mon Sep 17 00:00:00 2001 From: Robus Gauli Date: Thu, 9 Apr 2020 13:07:08 +0545 Subject: [PATCH 06/24] Remove docker-compose file with postgres deps --- .travis.yml | 11 +++++------ docker-compose.yml | 41 ----------------------------------------- package.json | 8 ++------ 3 files changed, 7 insertions(+), 53 deletions(-) delete mode 100644 docker-compose.yml diff --git a/.travis.yml b/.travis.yml index d5c084f..69c416d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,7 +6,7 @@ node_js: - lts/dubnium services: - - postgresql + - mysql branches: only: @@ -14,13 +14,13 @@ branches: env: > NODE_ENV=test - APP_NAME='Express API ES6 Starter' + APP_NAME='Node JS starter' APP_VERSION='1.0.0' TEST_APP_PORT='9945' - TEST_DB_NAME='express_test' + TEST_DB_NAME='app_test' TEST_DB_PASSWORD='' - TEST_DB_PORT='5432' - TEST_DB_USER='postgres' + TEST_DB_PORT='3306' + TEST_DB_USER='mysql' before_install: - curl -o- -L https://yarnpkg.com/install.sh | bash @@ -28,7 +28,6 @@ before_install: before_script: - cp .env.example .env - - psql -c 'create database express_test;' -U postgres - yarn migrate script: diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index bb1d2db..0000000 --- a/docker-compose.yml +++ /dev/null @@ -1,41 +0,0 @@ -version: '3.4' -services: - express-api-es6-starter: - build: - context: . - target: dev - volumes: - - .env.docker:/app/.env - ports: - - '8848:8848' - depends_on: - - pg - - pg_test - links: - - pg - - pg_test - - migration: - build: - context: . - target: migrate - volumes: - - .env.docker:/app/.env - depends_on: - - pg - - pg_test - links: - - pg - - pg_test - pg: - image: postgres:11-alpine - environment: - - POSTGRES_USER=postgres - - POSTGRES_PASSWORD=mysecretpassword - - POSTGRES_DB=express - pg_test: - image: postgres:11-alpine - environment: - - POSTGRES_USER=postgres - - POSTGRES_PASSWORD=mysecretpassword - - POSTGRES_DB=express_test diff --git a/package.json b/package.json index bb107e3..dbee922 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { - "name": "express-api-es6-starter", + "name": "NodeJS starter", "version": "1.0.0", - "description": "Express API ES6 Starter", + "description": "NodeJS Starter", "scripts": { "start": "node dist", "prestart": "yarn build", @@ -42,7 +42,6 @@ "api" ], "private": true, - "author": "Saugat Acharya ", "license": "MIT", "dependencies": { "@hapi/boom": "^9.1.0", @@ -50,9 +49,6 @@ "@sentry/node": "^5.15.0", "axios": "^0.19.2", "body-parser": "^1.19.0", - "bookshelf": "^1.1.0", - "bookshelf-virtuals-plugin": "^0.1.1", - "boom": "^7.3.0", "compression": "^1.7.4", "continuation-local-storage": "^3.2.1", "cors": "^2.8.5", From 49d37e4f095df9f0a1a01bc96f34b5ab303191e3 Mon Sep 17 00:00:00 2001 From: Robus Gauli Date: Thu, 9 Apr 2020 13:07:33 +0545 Subject: [PATCH 07/24] Rename package json name --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index dbee922..4b13c0f 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "NodeJS starter", + "name": "starter", "version": "1.0.0", "description": "NodeJS Starter", "scripts": { From 3d80d7767bd9209da6fb6d866257d0b872bbf9d5 Mon Sep 17 00:00:00 2001 From: Robus Gauli Date: Thu, 9 Apr 2020 13:25:20 +0545 Subject: [PATCH 08/24] Remove docker-compose instruction from readme --- README.md | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/README.md b/README.md index 00018c2..d98090f 100644 --- a/README.md +++ b/README.md @@ -37,27 +37,6 @@ Example, $ yarn make:migration create_tags_table $ yarn make:seeder 02_insert_tags -## Using Docker - -### Using docker-compose - -Use [docker-compose](https://docs.docker.com/compose/) to quickly bring up a stack with pre-configured Postgres database container. Data is ephemeral and containers will disappear when stack is removed. - -Specific configuration for Docker is in `.env.docker` - -- `0.0.0.0` as `$APP_HOST` to expose app on Docker network interface -- Pre-configured Postgres settings - can be updated to point to another Postgres host - -Bring up stack, - - $ docker-compose up - -Navigate to http://localhost:8848/api-docs/ to verify application is running from docker. - -Bring down stack, - - $ docker-compose down - ### Multi-stage docker builds There are multiple build targets available for different stages. These images can be used to deploy or run jobs in different container based cloud infrastructure like Kubernetes, AWS ECS, Fargate, GCP Cloud Run etc. From a1ce2e077efd5b40eadd7b8ad340491e7932b254 Mon Sep 17 00:00:00 2001 From: Robus Gauli Date: Thu, 9 Apr 2020 13:58:16 +0545 Subject: [PATCH 09/24] fix lint issues --- src/auth/index.js | 30 +++++++++++++++--------------- src/errors/database.js | 9 ++++----- src/errors/error.js | 5 +++-- src/errors/network.js | 12 ++++++------ src/errors/token.js | 12 ++++++------ src/models/model.js | 8 ++++---- src/services/user.js | 14 +++++++------- src/utils/buildError.js | 8 ++++++++ 8 files changed, 53 insertions(+), 45 deletions(-) diff --git a/src/auth/index.js b/src/auth/index.js index 41af31c..5f32005 100644 --- a/src/auth/index.js +++ b/src/auth/index.js @@ -56,30 +56,30 @@ export function fetchUserByToken(token) { * @param {Object} next */ function authenticateUser(req, res, next) { - try { - userSession.run(async () => { + userSession.run(async () => { + try { const token = getTokenFromHeaders(req); + const user = await fetchUserByToken(token); userSession.set('user', { ...user, token, }); - next(); - }); - } catch (err) { - logger.error(err); - if (err instanceof NetworkError) { - return next( - new NetworkError({ - message: INTERNAL_ERROR, - code: HttpStatus.INTERNAL_SERVER_ERROR, - }) - ); + } catch (err) { + logger.error(err); + if (err instanceof NetworkError) { + return next( + new NetworkError({ + message: INTERNAL_ERROR, + code: HttpStatus.INTERNAL_SERVER_ERROR, + }) + ); + } + next(err); } - next(err); - } + }); } export default authenticateUser; diff --git a/src/errors/database.js b/src/errors/database.js index 76ce5d7..0662d88 100644 --- a/src/errors/database.js +++ b/src/errors/database.js @@ -7,7 +7,6 @@ import BaseError from './error'; * Error class for database failure and error. */ class DatabaseError extends BaseError { - /** * Constructor of DatabaseError. * @@ -20,18 +19,18 @@ class DatabaseError extends BaseError { super(message); this.title = title; this.message = message; - this.code = code; + this.statusCode = code; } /** * Returns the formatted string representation of error. - * + * * @method DatabaseError#toString - * + * * @returns {String} */ toString() { - return `${this.title}[${this.code}]`; + return `${this.title}[${this.statusCode}]`; } } diff --git a/src/errors/error.js b/src/errors/error.js index 3d38fdc..4607715 100644 --- a/src/errors/error.js +++ b/src/errors/error.js @@ -2,14 +2,15 @@ * Base class for error. */ class BaseError extends Error { - /** * Constructor method for BaseError. - * + * * @returns {BaseError} */ constructor(message) { super(message); + // This flag it used to distinguished from other error types such as joi, boom, etc. + this.isCustom = true; } } diff --git a/src/errors/network.js b/src/errors/network.js index b4ef9f8..f7ae467 100644 --- a/src/errors/network.js +++ b/src/errors/network.js @@ -13,25 +13,25 @@ class NetworkError extends BaseError { * @param {String} error.title * @param {String} error.message * @param {Number} error.code - * + * * @returns {NetworkError} */ constructor({ title = TITLE, message = '', code = 500 }) { super(message); this.title = title; this.message = message; - this.code = code; + this.statusCode = code; } - + /** * Returns the formatted string representation of error. - * + * * @method NetworkError#toString - * + * * @returns {String} */ toString() { - return `${this.title}[${this.code}]`; + return `${this.title}[${this.statusCode}]`; } } diff --git a/src/errors/token.js b/src/errors/token.js index 1240f2c..4cb5c8c 100644 --- a/src/errors/token.js +++ b/src/errors/token.js @@ -13,25 +13,25 @@ class TokenError extends BaseError { * @param {String} error.title * @param {String} error.message * @param {Number} error.code - * + * * @returns {TokenError} */ constructor({ title = TITLE, message = '', code = 401 }) { super(message); this.title = title; - this.message = message; - this.code = code; + this.message = message || title; + this.statusCode = code; } /** * Returns the formatted string representation of error. - * + * * @method TokenError#toString - * + * * @returns {String} */ toString() { - return `${this.title} [${this.code}]`; + return `${this.title} [${this.statusCode}]`; } } diff --git a/src/models/model.js b/src/models/model.js index ed9473d..d022651 100644 --- a/src/models/model.js +++ b/src/models/model.js @@ -12,7 +12,7 @@ class Model { /** * This method is required by the domain class. - * + * * @returns {String} */ getTable() { @@ -21,9 +21,9 @@ class Model { /** * This method persists the payload object to underlying database. - * - * @param {Object} payload - * + * + * @param {Object} payload + * * @returns {Promise} */ save(payload = {}) { diff --git a/src/services/user.js b/src/services/user.js index 32ed1a1..a966abe 100644 --- a/src/services/user.js +++ b/src/services/user.js @@ -16,17 +16,17 @@ export async function fetchAll() { /** * Create a new user. - * + * * @returns {Promise} */ export async function create() { const user = userSession(); - const id = await new User().save(user); + const id = await new User().save(user); + + logger.info(`User created: ${user}`); - logger.info(`User created: ${user}`); - - return { - id: id.pop(), - }; + return { + id: id.pop(), + }; } diff --git a/src/utils/buildError.js b/src/utils/buildError.js index ef32b04..4d97ef6 100644 --- a/src/utils/buildError.js +++ b/src/utils/buildError.js @@ -31,6 +31,14 @@ function buildError(err) { }; } + // Custom errors + if (err.isCustom) { + return { + code: err.statusCode, + message: err.message + }; + } + // Return INTERNAL_SERVER_ERROR for all other cases return { code: HttpStatus.INTERNAL_SERVER_ERROR, From ec3668c9bc6b8e8f4efce5d78c0a42c891051b05 Mon Sep 17 00:00:00 2001 From: Robus Gauli Date: Thu, 9 Apr 2020 14:04:10 +0545 Subject: [PATCH 10/24] Use async/await keyword instead of 'then' --- src/controllers/user.js | 17 ++++++++++------- src/utils/buildError.js | 2 +- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/controllers/user.js b/src/controllers/user.js index af5e977..b8235c6 100644 --- a/src/controllers/user.js +++ b/src/controllers/user.js @@ -11,11 +11,14 @@ import * as userService from '../services/user'; * @param {Object} res * @param {Function} next */ -export function fetchAll(req, res, next) { - userService - .fetchAll() - .then((data) => res.json({ data })) - .catch(next); +export async function fetchAll(req, res, next) { + try { + const data = await userService.fetchAll(); + res.json({ data }); + } catch (err) { + logger.error(err); + next(err); + } } /** @@ -25,9 +28,9 @@ export function fetchAll(req, res, next) { * @param {Object} res * @param {Function} next */ -export function create(req, res, next) { +export async function create(req, res, next) { try { - const data = userService.create(req.body); + const data = await userService.create(req.body); res.status(HttpStatus.CREATED).json({ data }); } catch (err) { diff --git a/src/utils/buildError.js b/src/utils/buildError.js index 4d97ef6..c881c29 100644 --- a/src/utils/buildError.js +++ b/src/utils/buildError.js @@ -35,7 +35,7 @@ function buildError(err) { if (err.isCustom) { return { code: err.statusCode, - message: err.message + message: err.message, }; } From ae88dac2a46064bbdb117acf3028776b7941f059 Mon Sep 17 00:00:00 2001 From: Robus Gauli Date: Thu, 9 Apr 2020 15:09:08 +0545 Subject: [PATCH 11/24] Add error formatter in winston --- src/middlewares/errorHandler.js | 2 -- src/utils/logger.js | 19 ++++++++++++++++--- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/src/middlewares/errorHandler.js b/src/middlewares/errorHandler.js index a348359..130ff4f 100644 --- a/src/middlewares/errorHandler.js +++ b/src/middlewares/errorHandler.js @@ -63,8 +63,6 @@ export function bodyParser(err, req, res, next) { * @param {Function} next */ export function genericErrorHandler(err, req, res, next) { - logger.error(err.stack); const error = buildError(err); - res.status(error.code).json({ error }); } diff --git a/src/utils/logger.js b/src/utils/logger.js index edb51fa..19a9b53 100644 --- a/src/utils/logger.js +++ b/src/utils/logger.js @@ -1,6 +1,7 @@ import fs from 'fs'; -import winston, { format } from 'winston'; +import path from 'path'; +import winston, { format } from 'winston'; import 'winston-daily-rotate-file'; // Use LOG_DIR from env @@ -12,14 +13,26 @@ if (!fs.existsSync(LOG_DIR)) { fs.mkdirSync(LOG_DIR); } +// logFormat used for console logging +const logFormat = format.printf(info => `${info.timestamp} ${info.level} [${info.label}]: ${info.message}`); + /** * Create a new winston logger. */ const logger = winston.createLogger({ + format: format.combine( + format.label({ label: path.basename(process.mainModule.filename) }), + format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), + // Format the metadata object + format.metadata({ fillExcept: ['message', 'level', 'timestamp', 'label'] }) + ), transports: [ new winston.transports.Console({ - format: format.combine(format.colorize(), format.simple()), - level: 'info', + format: format.combine( + format.colorize(), + logFormat, + ), + level: "info", }), new winston.transports.DailyRotateFile({ format: format.combine(format.timestamp(), format.json()), From 8f313f0d974b5f86ece42e3515ab1f9060c99817 Mon Sep 17 00:00:00 2001 From: Robus Gauli Date: Thu, 9 Apr 2020 15:16:36 +0545 Subject: [PATCH 12/24] Change api of model --- src/models/model.js | 5 +++++ src/models/user.js | 2 +- src/routes.js | 2 +- src/services/user.js | 4 ++-- 4 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/models/model.js b/src/models/model.js index d022651..0ceac46 100644 --- a/src/models/model.js +++ b/src/models/model.js @@ -47,6 +47,11 @@ class Model { }); }); } + + fetchAll() { + return this._db.select('*'); + } + } export default Model; diff --git a/src/models/user.js b/src/models/user.js index 3b301c8..842ae5c 100644 --- a/src/models/user.js +++ b/src/models/user.js @@ -11,4 +11,4 @@ class User extends Model { } } -export default User; +export default new User(); diff --git a/src/routes.js b/src/routes.js index 3699b3c..ba15538 100644 --- a/src/routes.js +++ b/src/routes.js @@ -20,7 +20,7 @@ router.get('/swagger.json', (req, res) => { /** * LMS Authentication middleware */ -router.use(authenticateUser); +// router.use(authenticateUser); /** * GET /api diff --git a/src/services/user.js b/src/services/user.js index a966abe..8801120 100644 --- a/src/services/user.js +++ b/src/services/user.js @@ -9,7 +9,7 @@ import logger from '../utils/logger'; * @returns {Promise} */ export async function fetchAll() { - const users = await new User().fetchAll(); + const users = await User.fetchAll(); return users; } @@ -22,7 +22,7 @@ export async function fetchAll() { export async function create() { const user = userSession(); - const id = await new User().save(user); + const id = await User.save(user); logger.info(`User created: ${user}`); From 6f9b0e8734565c2c23e9377ed53777c38006adf2 Mon Sep 17 00:00:00 2001 From: Robus Gauli Date: Fri, 10 Apr 2020 11:21:46 +0545 Subject: [PATCH 13/24] Remove status code from custom error type --- .env.example | 1 + src/auth/index.js | 70 ++++++++++++++++----------------- src/controllers/user.js | 1 + src/errors/database.js | 9 +---- src/errors/error.js | 14 ++++++- src/errors/network.js | 6 +-- src/errors/token.js | 17 +++++--- src/middlewares/errorHandler.js | 1 + src/middlewares/json.js | 4 +- src/models/model.js | 5 --- src/routes.js | 2 +- src/utils/buildError.js | 2 +- src/utils/logger.js | 50 ++++++++++++++--------- 13 files changed, 99 insertions(+), 83 deletions(-) diff --git a/.env.example b/.env.example index 109623f..2e9c559 100644 --- a/.env.example +++ b/.env.example @@ -6,6 +6,7 @@ APP_HOST='127.0.0.1' # Log LOG_LEVEL='debug' +ENABLE_FILE_LOG='TRUE' # Database DB_CLIENT='mysql' diff --git a/src/auth/index.js b/src/auth/index.js index 5f32005..fc9a04c 100644 --- a/src/auth/index.js +++ b/src/auth/index.js @@ -1,51 +1,51 @@ import HttpStatus from 'http-status-codes'; import TokenError from '../errors/token'; -import NetworkError from '../errors/network'; import { http } from '../utils/http'; -import logger from '../utils/logger'; import { userSession } from './session'; -const INTERNAL_ERROR = 'Internal Error'; - /** - * Get token from header in request. + * Get token from header in http request. * * @param {Object} req */ -const getTokenFromHeaders = (req) => { +function getTokenFromHeaders(req) { const { - headers: { authorization }, + headers: { authorization = '' }, } = req; - if (authorization && authorization.split(' ')[0] === 'Bearer') { - if (authorization.split(' ')[1] !== undefined) { - return authorization.split(' ')[1]; - } - } + const fields = authorization.split(' ').filter(Boolean); - throw new TokenError({ - code: HttpStatus.UNAUTHORIZED, - }); -}; + if (fields.length <= 1 || fields[0] !== 'Bearer') { + return { + ok: false, + }; + } + + return { + token: fields[1], + }; +} /** - * Fetch users from auth server from token. + * Fetch user from auth server from token. * * @param {String} token * @throws NetworkError + * + * @returns {Promise} */ -export function fetchUserByToken(token) { - return http - .get(`${process.env.AUTH_URL}/userinfo`, { - headers: { - accessToken: token, - clientId: process.env.AUTH_CLIENT_ID, - }, - }) - .then((response) => response.data); +async function fetchUserByToken(token) { + const { data } = await http.get(`${process.env.AUTH_URL}/userinfo`, { + headers: { + accessToken: token, + clientId: process.env.AUTH_CLIENT_ID, + }, + }); + + return data; } /** @@ -58,8 +58,13 @@ export function fetchUserByToken(token) { function authenticateUser(req, res, next) { userSession.run(async () => { try { - const token = getTokenFromHeaders(req); - + const { ok, token } = getTokenFromHeaders(req); + + if (!ok) { + throw new TokenError({ + code: HttpStatus.UNAUTHORIZED, + }); + } const user = await fetchUserByToken(token); userSession.set('user', { @@ -68,15 +73,6 @@ function authenticateUser(req, res, next) { }); next(); } catch (err) { - logger.error(err); - if (err instanceof NetworkError) { - return next( - new NetworkError({ - message: INTERNAL_ERROR, - code: HttpStatus.INTERNAL_SERVER_ERROR, - }) - ); - } next(err); } }); diff --git a/src/controllers/user.js b/src/controllers/user.js index b8235c6..1baa58e 100644 --- a/src/controllers/user.js +++ b/src/controllers/user.js @@ -14,6 +14,7 @@ import * as userService from '../services/user'; export async function fetchAll(req, res, next) { try { const data = await userService.fetchAll(); + res.json({ data }); } catch (err) { logger.error(err); diff --git a/src/errors/database.js b/src/errors/database.js index 0662d88..b5aafba 100644 --- a/src/errors/database.js +++ b/src/errors/database.js @@ -1,9 +1,6 @@ import BaseError from './error'; /** - * @class DatabaseError - * @extends BaseError - * * Error class for database failure and error. */ class DatabaseError extends BaseError { @@ -13,20 +10,16 @@ class DatabaseError extends BaseError { * @param {Object} error * @param {String} error.title * @param {String} error.message - * @param {Number} error.code */ - constructor({ title = '', message = '', code = 500 }) { + constructor({ title = '', message = '' }) { super(message); this.title = title; this.message = message; - this.statusCode = code; } /** * Returns the formatted string representation of error. * - * @method DatabaseError#toString - * * @returns {String} */ toString() { diff --git a/src/errors/error.js b/src/errors/error.js index 4607715..adf6398 100644 --- a/src/errors/error.js +++ b/src/errors/error.js @@ -1,3 +1,4 @@ +import HttpStatus from 'http-status-codes'; /** * Base class for error. */ @@ -5,13 +6,24 @@ class BaseError extends Error { /** * Constructor method for BaseError. * + * @param {String} message + * * @returns {BaseError} */ constructor(message) { super(message); - // This flag it used to distinguished from other error types such as joi, boom, etc. + // This flag is used to distinguished from other error types such as joi, boom, etc. this.isCustom = true; } + + /** + * Generic http status code for custom errors. + * + * @returns {Number} + */ + httpCode() { + return HttpStatus.INTERNAL_SERVER_ERROR; + } } export default BaseError; diff --git a/src/errors/network.js b/src/errors/network.js index f7ae467..3de6fdf 100644 --- a/src/errors/network.js +++ b/src/errors/network.js @@ -12,22 +12,18 @@ class NetworkError extends BaseError { * @param {Object} error * @param {String} error.title * @param {String} error.message - * @param {Number} error.code * * @returns {NetworkError} */ - constructor({ title = TITLE, message = '', code = 500 }) { + constructor({ title = TITLE, message = '' }) { super(message); this.title = title; this.message = message; - this.statusCode = code; } /** * Returns the formatted string representation of error. * - * @method NetworkError#toString - * * @returns {String} */ toString() { diff --git a/src/errors/token.js b/src/errors/token.js index 4cb5c8c..c520184 100644 --- a/src/errors/token.js +++ b/src/errors/token.js @@ -1,3 +1,5 @@ +import HttpStatus from 'http-status-codes'; + import BaseError from './error'; const TITLE = 'Invalid access token'; @@ -12,27 +14,32 @@ class TokenError extends BaseError { * @param {Object} error * @param {String} error.title * @param {String} error.message - * @param {Number} error.code * * @returns {TokenError} */ - constructor({ title = TITLE, message = '', code = 401 }) { + constructor({ title = TITLE, message = '' }) { super(message); this.title = title; this.message = message || title; - this.statusCode = code; } /** * Returns the formatted string representation of error. * - * @method TokenError#toString - * * @returns {String} */ toString() { return `${this.title} [${this.statusCode}]`; } + + /** + * Returns http status code for invalid token. + * + * @returns {Number} + */ + httpCode() { + return HttpStatus.UNAUTHORIZED; + } } export default TokenError; diff --git a/src/middlewares/errorHandler.js b/src/middlewares/errorHandler.js index 130ff4f..9f65d2d 100644 --- a/src/middlewares/errorHandler.js +++ b/src/middlewares/errorHandler.js @@ -64,5 +64,6 @@ export function bodyParser(err, req, res, next) { */ export function genericErrorHandler(err, req, res, next) { const error = buildError(err); + res.status(error.code).json({ error }); } diff --git a/src/middlewares/json.js b/src/middlewares/json.js index e7bead1..f0ef96a 100644 --- a/src/middlewares/json.js +++ b/src/middlewares/json.js @@ -5,10 +5,10 @@ import _isEmpty from 'lodash/isEmpty'; * Middleware to handle empty JSON body requests and other edge cases if any. * * @param {Object} request - * @param {Object} response + * @param {Object} _ * @param {Function} next */ -export default function json(request, response, next) { +export default function json(request, _, next) { const { body, method } = request; const disallowedHttpHeaders = ['PUT', 'POST', 'PATCH']; diff --git a/src/models/model.js b/src/models/model.js index 0ceac46..d022651 100644 --- a/src/models/model.js +++ b/src/models/model.js @@ -47,11 +47,6 @@ class Model { }); }); } - - fetchAll() { - return this._db.select('*'); - } - } export default Model; diff --git a/src/routes.js b/src/routes.js index ba15538..3699b3c 100644 --- a/src/routes.js +++ b/src/routes.js @@ -20,7 +20,7 @@ router.get('/swagger.json', (req, res) => { /** * LMS Authentication middleware */ -// router.use(authenticateUser); +router.use(authenticateUser); /** * GET /api diff --git a/src/utils/buildError.js b/src/utils/buildError.js index c881c29..6977fb6 100644 --- a/src/utils/buildError.js +++ b/src/utils/buildError.js @@ -34,7 +34,7 @@ function buildError(err) { // Custom errors if (err.isCustom) { return { - code: err.statusCode, + code: err.httpCode(), message: err.message, }; } diff --git a/src/utils/logger.js b/src/utils/logger.js index 19a9b53..3a764a4 100644 --- a/src/utils/logger.js +++ b/src/utils/logger.js @@ -8,13 +8,15 @@ import 'winston-daily-rotate-file'; const LOG_DIR = process.env.LOG_DIR || 'logs'; const LOG_LEVEL = process.env.LOG_LEVEL || 'info'; +const isFileLogEnabled = process.env.ENABLE_FILE_LOG === 'TRUE'; + // Create log directory if it does not exist if (!fs.existsSync(LOG_DIR)) { fs.mkdirSync(LOG_DIR); } // logFormat used for console logging -const logFormat = format.printf(info => `${info.timestamp} ${info.level} [${info.label}]: ${info.message}`); +const logFormat = format.printf((info) => `${info.timestamp} ${info.level} [${info.label}]: ${info.message}`); /** * Create a new winston logger. @@ -26,25 +28,37 @@ const logger = winston.createLogger({ // Format the metadata object format.metadata({ fillExcept: ['message', 'level', 'timestamp', 'label'] }) ), - transports: [ - new winston.transports.Console({ - format: format.combine( - format.colorize(), - logFormat, - ), - level: "info", - }), - new winston.transports.DailyRotateFile({ - format: format.combine(format.timestamp(), format.json()), - maxFiles: '14d', - level: LOG_LEVEL, - dirname: LOG_DIR, - datePattern: 'YYYY-MM-DD', - filename: '%DATE%-debug.log', - }), - ], + transports: setupTransports(), }); +/** + * Setup transports for winston. + */ +function setupTransports() { + const transports = []; + + transports.push( + new winston.transports.Console({ + format: format.combine(format.colorize(), logFormat), + level: 'info', + }) + ); + if (isFileLogEnabled) { + transports.push( + new winston.transports.DailyRotateFile({ + format: format.combine(format.timestamp(), format.json()), + maxFiles: '14d', + level: LOG_LEVEL, + dirname: LOG_DIR, + datePattern: 'YYYY-MM-DD', + filename: '%DATE%-debug.log', + }) + ); + } + + return transports; +} + export const logStream = { /** * A writable stream for winston logger. From 7ec80a977e738f302c994386d195858532b2ef28 Mon Sep 17 00:00:00 2001 From: Robus Gauli Date: Fri, 10 Apr 2020 13:10:06 +0545 Subject: [PATCH 14/24] Add option to namespace the log --- src/auth/index.js | 16 ++++++++-------- src/controllers/user.js | 2 +- src/errors/database.js | 15 +-------------- src/errors/error.js | 5 +++-- src/errors/network.js | 19 +------------------ src/errors/token.js | 19 +------------------ src/middlewares/errorHandler.js | 2 +- src/utils/logger.js | 30 ++++++++++++++++++++++++++---- 8 files changed, 42 insertions(+), 66 deletions(-) diff --git a/src/auth/index.js b/src/auth/index.js index fc9a04c..36225ea 100644 --- a/src/auth/index.js +++ b/src/auth/index.js @@ -1,11 +1,12 @@ -import HttpStatus from 'http-status-codes'; - import TokenError from '../errors/token'; +import logger from '../utils/logger'; import { http } from '../utils/http'; import { userSession } from './session'; +const log = logger.withNamespace('AUTH'); + /** * Get token from header in http request. * @@ -23,7 +24,7 @@ function getTokenFromHeaders(req) { ok: false, }; } - + return { token: fields[1], }; @@ -44,7 +45,7 @@ async function fetchUserByToken(token) { clientId: process.env.AUTH_CLIENT_ID, }, }); - + return data; } @@ -59,11 +60,9 @@ function authenticateUser(req, res, next) { userSession.run(async () => { try { const { ok, token } = getTokenFromHeaders(req); - + if (!ok) { - throw new TokenError({ - code: HttpStatus.UNAUTHORIZED, - }); + throw new TokenError('Invalid token'); } const user = await fetchUserByToken(token); @@ -73,6 +72,7 @@ function authenticateUser(req, res, next) { }); next(); } catch (err) { + log.error(err); next(err); } }); diff --git a/src/controllers/user.js b/src/controllers/user.js index 1baa58e..07b0163 100644 --- a/src/controllers/user.js +++ b/src/controllers/user.js @@ -14,7 +14,7 @@ import * as userService from '../services/user'; export async function fetchAll(req, res, next) { try { const data = await userService.fetchAll(); - + res.json({ data }); } catch (err) { logger.error(err); diff --git a/src/errors/database.js b/src/errors/database.js index b5aafba..1ef7c07 100644 --- a/src/errors/database.js +++ b/src/errors/database.js @@ -4,26 +4,13 @@ import BaseError from './error'; * Error class for database failure and error. */ class DatabaseError extends BaseError { - /** - * Constructor of DatabaseError. - * - * @param {Object} error - * @param {String} error.title - * @param {String} error.message - */ - constructor({ title = '', message = '' }) { - super(message); - this.title = title; - this.message = message; - } - /** * Returns the formatted string representation of error. * * @returns {String} */ toString() { - return `${this.title}[${this.statusCode}]`; + return `Database Error: ${this.message}`; } } diff --git a/src/errors/error.js b/src/errors/error.js index adf6398..df4f893 100644 --- a/src/errors/error.js +++ b/src/errors/error.js @@ -1,4 +1,5 @@ import HttpStatus from 'http-status-codes'; + /** * Base class for error. */ @@ -6,11 +7,11 @@ class BaseError extends Error { /** * Constructor method for BaseError. * - * @param {String} message + * @param {String} message * * @returns {BaseError} */ - constructor(message) { + constructor(message = '') { super(message); // This flag is used to distinguished from other error types such as joi, boom, etc. this.isCustom = true; diff --git a/src/errors/network.js b/src/errors/network.js index 3de6fdf..b37ffbc 100644 --- a/src/errors/network.js +++ b/src/errors/network.js @@ -1,33 +1,16 @@ import BaseError from './error'; -const TITLE = 'Network error'; - /** * Error class for Network error. */ class NetworkError extends BaseError { - /** - * Constructor for NetworkError. - * - * @param {Object} error - * @param {String} error.title - * @param {String} error.message - * - * @returns {NetworkError} - */ - constructor({ title = TITLE, message = '' }) { - super(message); - this.title = title; - this.message = message; - } - /** * Returns the formatted string representation of error. * * @returns {String} */ toString() { - return `${this.title}[${this.statusCode}]`; + return `Network Error: ${this.message}`; } } diff --git a/src/errors/token.js b/src/errors/token.js index c520184..d27146e 100644 --- a/src/errors/token.js +++ b/src/errors/token.js @@ -2,34 +2,17 @@ import HttpStatus from 'http-status-codes'; import BaseError from './error'; -const TITLE = 'Invalid access token'; - /** * Error class for Token Error. */ class TokenError extends BaseError { - /** - * Constructor of TokenError. - * - * @param {Object} error - * @param {String} error.title - * @param {String} error.message - * - * @returns {TokenError} - */ - constructor({ title = TITLE, message = '' }) { - super(message); - this.title = title; - this.message = message || title; - } - /** * Returns the formatted string representation of error. * * @returns {String} */ toString() { - return `${this.title} [${this.statusCode}]`; + return `Token Error: ${this.message}`; } /** diff --git a/src/middlewares/errorHandler.js b/src/middlewares/errorHandler.js index 9f65d2d..d255b2b 100644 --- a/src/middlewares/errorHandler.js +++ b/src/middlewares/errorHandler.js @@ -64,6 +64,6 @@ export function bodyParser(err, req, res, next) { */ export function genericErrorHandler(err, req, res, next) { const error = buildError(err); - + res.status(error.code).json({ error }); } diff --git a/src/utils/logger.js b/src/utils/logger.js index 3a764a4..2c0d699 100644 --- a/src/utils/logger.js +++ b/src/utils/logger.js @@ -16,7 +16,15 @@ if (!fs.existsSync(LOG_DIR)) { } // logFormat used for console logging -const logFormat = format.printf((info) => `${info.timestamp} ${info.level} [${info.label}]: ${info.message}`); +const logFormat = format.printf((info) => { + let namespace = ''; + + if (info.metadata.namespace) { + namespace = `[${info.metadata.namespace}]`; + } + + return `${info.timestamp} [${info.level}] [${info.label}] ${namespace}: ${info.message}`; +}); /** * Create a new winston logger. @@ -26,20 +34,34 @@ const logger = winston.createLogger({ format.label({ label: path.basename(process.mainModule.filename) }), format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), // Format the metadata object - format.metadata({ fillExcept: ['message', 'level', 'timestamp', 'label'] }) + format.metadata({ fillExcept: ['message', 'level', 'timestamp', 'label'] }), + logFormat ), transports: setupTransports(), }); +/** + * Creates a child logger with namespace for logging. + * + * @param {String} namespace + * + * @returns {Object} + */ +logger.withNamespace = function (namespace) { + return logger.child({ namespace }); +}; + /** * Setup transports for winston. + * + * @returns {Array} */ function setupTransports() { const transports = []; - + transports.push( new winston.transports.Console({ - format: format.combine(format.colorize(), logFormat), + format: format.combine(format.colorize()), level: 'info', }) ); From 5a0a2e38496820db3247632149f01a7e274284c3 Mon Sep 17 00:00:00 2001 From: Robus Gauli Date: Fri, 10 Apr 2020 14:11:24 +0545 Subject: [PATCH 15/24] Integrate async-store for requestID injection --- package.json | 2 +- src/auth/index.js | 39 ++++++-------- src/index.js | 4 ++ src/middlewares/errorHandler.js | 1 + src/services/user.js | 7 +-- src/utils/buildError.js | 7 +++ src/utils/logger.js | 13 +++-- yarn.lock | 93 +++++++-------------------------- 8 files changed, 62 insertions(+), 104 deletions(-) diff --git a/package.json b/package.json index 4b13c0f..3d4a203 100644 --- a/package.json +++ b/package.json @@ -46,11 +46,11 @@ "dependencies": { "@hapi/boom": "^9.1.0", "@hapi/joi": "^17.1.1", + "@leapfrogtechnology/async-store": "^1.2.0", "@sentry/node": "^5.15.0", "axios": "^0.19.2", "body-parser": "^1.19.0", "compression": "^1.7.4", - "continuation-local-storage": "^3.2.1", "cors": "^2.8.5", "dotenv": "^8.2.0", "express": "^4.17.1", diff --git a/src/auth/index.js b/src/auth/index.js index 36225ea..cf52a3d 100644 --- a/src/auth/index.js +++ b/src/auth/index.js @@ -1,11 +1,7 @@ -import TokenError from '../errors/token'; +import * as store from '@leapfrogtechnology/async-store'; -import logger from '../utils/logger'; import { http } from '../utils/http'; - -import { userSession } from './session'; - -const log = logger.withNamespace('AUTH'); +import TokenError from '../errors/token'; /** * Get token from header in http request. @@ -56,26 +52,21 @@ async function fetchUserByToken(token) { * @param {Object} res * @param {Object} next */ -function authenticateUser(req, res, next) { - userSession.run(async () => { - try { - const { ok, token } = getTokenFromHeaders(req); - - if (!ok) { - throw new TokenError('Invalid token'); - } - const user = await fetchUserByToken(token); +async function authenticateUser(req, res, next) { + try { + const { ok, token } = getTokenFromHeaders(req); - userSession.set('user', { - ...user, - token, - }); - next(); - } catch (err) { - log.error(err); - next(err); + if (!ok) { + throw new TokenError('Invalid token'); } - }); + + const user = await fetchUserByToken(token); + + store.set(user); + next(); + } catch (err) { + next(err); + } } export default authenticateUser; diff --git a/src/index.js b/src/index.js index e3f7ce2..f9078c9 100644 --- a/src/index.js +++ b/src/index.js @@ -11,6 +11,7 @@ import favicon from 'serve-favicon'; import bodyParser from 'body-parser'; import compression from 'compression'; import * as Sentry from '@sentry/node'; +import * as store from '@leapfrogtechnology/async-store'; import routes from './routes'; import json from './middlewares/json'; @@ -38,6 +39,9 @@ app.locals.version = process.env.APP_VERSION; // This request handler must be the first middleware on the app app.use(Sentry.Handlers.requestHandler()); +// For context propagation of request-response http roundtrip +app.use(store.initializeMiddleware()); + app.use(favicon(path.join(__dirname, '/../public', 'favicon.ico'))); app.use(cors()); app.use(helmet()); diff --git a/src/middlewares/errorHandler.js b/src/middlewares/errorHandler.js index d255b2b..a348359 100644 --- a/src/middlewares/errorHandler.js +++ b/src/middlewares/errorHandler.js @@ -63,6 +63,7 @@ export function bodyParser(err, req, res, next) { * @param {Function} next */ export function genericErrorHandler(err, req, res, next) { + logger.error(err.stack); const error = buildError(err); res.status(error.code).json({ error }); diff --git a/src/services/user.js b/src/services/user.js index 8801120..bf9bbdd 100644 --- a/src/services/user.js +++ b/src/services/user.js @@ -1,6 +1,6 @@ -import User from '../models/user'; -import userSession from '../auth/session'; +import * as store from '@leapfrogtechnology/async-store'; +import User from '../models/user'; import logger from '../utils/logger'; /** @@ -20,7 +20,8 @@ export async function fetchAll() { * @returns {Promise} */ export async function create() { - const user = userSession(); + // Example for retrieving the user from async-store in service. + const user = store.get('user'); const id = await User.save(user); diff --git a/src/utils/buildError.js b/src/utils/buildError.js index 6977fb6..be59172 100644 --- a/src/utils/buildError.js +++ b/src/utils/buildError.js @@ -1,4 +1,5 @@ import HttpStatus from 'http-status-codes'; +import * as store from '@leapfrogtechnology/async-store'; /** * Build error response for validation errors. @@ -7,9 +8,12 @@ import HttpStatus from 'http-status-codes'; * @returns {Object} */ function buildError(err) { + const requestID = store.getShortId(); + // Validation errors if (err.isJoi) { return { + id: requestID, code: HttpStatus.BAD_REQUEST, message: HttpStatus.getStatusText(HttpStatus.BAD_REQUEST), details: @@ -26,6 +30,7 @@ function buildError(err) { // HTTP errors if (err.isBoom) { return { + id: requestID, code: err.output.statusCode, message: err.output.payload.message || err.output.payload.error, }; @@ -34,6 +39,7 @@ function buildError(err) { // Custom errors if (err.isCustom) { return { + id: requestID, code: err.httpCode(), message: err.message, }; @@ -41,6 +47,7 @@ function buildError(err) { // Return INTERNAL_SERVER_ERROR for all other cases return { + id: requestID, code: HttpStatus.INTERNAL_SERVER_ERROR, message: HttpStatus.getStatusText(HttpStatus.INTERNAL_SERVER_ERROR), }; diff --git a/src/utils/logger.js b/src/utils/logger.js index 2c0d699..fd853d7 100644 --- a/src/utils/logger.js +++ b/src/utils/logger.js @@ -2,6 +2,8 @@ import fs from 'fs'; import path from 'path'; import winston, { format } from 'winston'; +import * as store from '@leapfrogtechnology/async-store'; + import 'winston-daily-rotate-file'; // Use LOG_DIR from env @@ -17,13 +19,18 @@ if (!fs.existsSync(LOG_DIR)) { // logFormat used for console logging const logFormat = format.printf((info) => { - let namespace = ''; + let formattedNamespace = ''; if (info.metadata.namespace) { - namespace = `[${info.metadata.namespace}]`; + formattedNamespace = `[${info.metadata.namespace}]`; } - return `${info.timestamp} [${info.level}] [${info.label}] ${namespace}: ${info.message}`; + // TODO: Will there be a situation when requestID would be empty string? + // May logs before middleware initialization? + const requestID = store.getShortId(); + const formattedReqID = requestID ? `[${requestID}] ` : ''; + + return `${info.timestamp} [${info.level}] [${info.label}] ${formattedReqID}${formattedNamespace}: ${info.message}`; }); /** diff --git a/yarn.lock b/yarn.lock index cdec250..f3650f5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -853,6 +853,15 @@ resolved "https://registry.yarnpkg.com/@istanbuljs/schema/-/schema-0.1.2.tgz#26520bf09abe4a5644cd5414e37125a8954241dd" integrity sha512-tsAQNx32a8CoFhjhijUIhI4kccIAgmGhy8LZMZgGfmXcpMbPRUqn5LWmgRttILi6yeGmBJd2xsPkFMs0PzgPCw== +"@leapfrogtechnology/async-store@^1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@leapfrogtechnology/async-store/-/async-store-1.2.0.tgz#a9162b4075159ed1452e5d48fa21049c508c20ac" + integrity sha512-u3LNseCs21d8w21OHrY6CY4FRIkeupgJQk65sYaZCO0GtNXBCz/YoS0qqWKnLtUrDWkIYiYusskO06XMg1UbiQ== + dependencies: + debug "4.1.1" + ramda "0.26.1" + uuid "3.3.2" + "@samverschueren/stream-to-observable@^0.3.0": version "0.3.0" resolved "https://registry.yarnpkg.com/@samverschueren/stream-to-observable/-/stream-to-observable-0.3.0.tgz#ecdf48d532c58ea477acfcab80348424f8d0662f" @@ -1180,14 +1189,6 @@ async-each@^1.0.1: resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.3.tgz#b727dbf87d7651602f06f4d4ac387f47d91b0cbf" integrity sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ== -async-listener@^0.6.0: - version "0.6.10" - resolved "https://registry.yarnpkg.com/async-listener/-/async-listener-0.6.10.tgz#a7c97abe570ba602d782273c0de60a51e3e17cbc" - integrity sha512-gpuo6xOyF4D5DE5WvyqZdPA3NGhiT6Qf07l7DCB0wwDEsLvDIbCr6j9S5aj5Ch96dLace5tXVzWBZkxU/c5ohw== - dependencies: - semver "^5.3.0" - shimmer "^1.1.0" - async@^2.6.1: version "2.6.3" resolved "https://registry.yarnpkg.com/async/-/async-2.6.3.tgz#d72625e2344a3656e3a3ad4fa749fa83299d82ff" @@ -1266,11 +1267,6 @@ bindings@^1.5.0: dependencies: file-uri-to-path "1.0.0" -bluebird@^3.7.2: - version "3.7.2" - resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" - integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== - body-parser@1.19.0, body-parser@^1.19.0: version "1.19.0" resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.0.tgz#96b2709e57c9c4e09a6fd66a8fd979844f69f08a" @@ -1287,30 +1283,6 @@ body-parser@1.19.0, body-parser@^1.19.0: raw-body "2.4.0" type-is "~1.6.17" -bookshelf-virtuals-plugin@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/bookshelf-virtuals-plugin/-/bookshelf-virtuals-plugin-0.1.1.tgz#cdd8c1609a4558a581ee684b5eadcf295afcf8ac" - integrity sha512-MFFjtzLoyWaGD7eMT2UZ8H00CHNk7M5WuSTxi+82gLJqbq0TyQdSQsKS5/NHfQjqd3wZKEV+yEbzpck2oBGe2A== - dependencies: - lodash "^4.17.15" - -bookshelf@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/bookshelf/-/bookshelf-1.1.0.tgz#d456abd70ed4af4c9ae6f3d364424590ea31dcbf" - integrity sha512-a4rrDI5pnjnj7xFAT23FJQyVxlrrvRPGUNRNj1upuBC5al7ObvoelTZbmVl9bnj+BZE5x19Y07+p933fCsBHNQ== - dependencies: - bluebird "^3.7.2" - create-error "~0.3.1" - inflection "^1.12.0" - lodash "^4.17.15" - -boom@^7.3.0: - version "7.3.0" - resolved "https://registry.yarnpkg.com/boom/-/boom-7.3.0.tgz#733a6d956d33b0b1999da3fe6c12996950d017b9" - integrity sha512-Swpoyi2t5+GhOEGw8rEsKvTxFLIDiiKoUc2gsoV6Lyr43LHBIzch3k2MvYUs8RTROrIkVJ3Al0TkaOGjnb+B6A== - dependencies: - hoek "6.x.x" - bowser@2.9.0: version "2.9.0" resolved "https://registry.yarnpkg.com/bowser/-/bowser-2.9.0.tgz#3bed854233b419b9a7422d9ee3e85504373821c9" @@ -1807,14 +1779,6 @@ content-type@~1.0.4: resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA== -continuation-local-storage@^3.2.1: - version "3.2.1" - resolved "https://registry.yarnpkg.com/continuation-local-storage/-/continuation-local-storage-3.2.1.tgz#11f613f74e914fe9b34c92ad2d28fe6ae1db7ffb" - integrity sha512-jx44cconVqkCEEyLSKWwkvUXwO561jXMa3LPjTPsm5QR22PA0/mhe33FT4Xb5y74JDvt/Cq+5lm8S8rskLv9ZA== - dependencies: - async-listener "^0.6.0" - emitter-listener "^1.1.1" - convert-source-map@^1.1.0, convert-source-map@^1.7.0: version "1.7.0" resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.7.0.tgz#17a2cb882d7f77d3490585e2ce6c524424a3a442" @@ -1896,11 +1860,6 @@ create-error-class@^3.0.0: dependencies: capture-stack-trace "^1.0.0" -create-error@~0.3.1: - version "0.3.1" - resolved "https://registry.yarnpkg.com/create-error/-/create-error-0.3.1.tgz#69810245a629e654432bf04377360003a5351a23" - integrity sha1-aYECRaYp5lRDK/BDdzYAA6U1GiM= - cross-env@^7.0.2: version "7.0.2" resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-7.0.2.tgz#bd5ed31339a93a3418ac4f3ca9ca3403082ae5f9" @@ -2143,13 +2102,6 @@ elegant-spinner@^1.0.1: resolved "https://registry.yarnpkg.com/elegant-spinner/-/elegant-spinner-1.0.1.tgz#db043521c95d7e303fd8f345bedc3349cfb0729e" integrity sha1-2wQ1IcldfjA/2PNFvtwzSc+wcp4= -emitter-listener@^1.1.1: - version "1.1.2" - resolved "https://registry.yarnpkg.com/emitter-listener/-/emitter-listener-1.1.2.tgz#56b140e8f6992375b3d7cb2cab1cc7432d9632e8" - integrity sha512-Bt1sBAGFHY9DKY+4/2cV6izcKJUf5T7/gkdmkxzX/qv9CcGH8xSwVRW5mtX03SWJtRTWSOpzCuWN9rBFYZepZQ== - dependencies: - shimmer "^1.2.0" - emoji-regex@^7.0.1: version "7.0.3" resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156" @@ -3073,11 +3025,6 @@ hide-powered-by@1.1.0: resolved "https://registry.yarnpkg.com/hide-powered-by/-/hide-powered-by-1.1.0.tgz#be3ea9cab4bdb16f8744be873755ca663383fa7a" integrity sha512-Io1zA2yOA1YJslkr+AJlWSf2yWFkKjvkcL9Ni1XSUqnGLr/qRQe2UI3Cn/J9MsJht7yEVCe0SscY1HgVMujbgg== -hoek@6.x.x: - version "6.1.3" - resolved "https://registry.yarnpkg.com/hoek/-/hoek-6.1.3.tgz#73b7d33952e01fe27a38b0457294b79dd8da242c" - integrity sha512-YXXAAhmF9zpQbC7LEcREFtXfGq5K1fmd+4PHkBq8NUqmzW3G+Dq10bI/i0KucLRwss3YYFQ0fSfoxBZYiGUqtQ== - homedir-polyfill@^1.0.1: version "1.0.3" resolved "https://registry.yarnpkg.com/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz#743298cef4e5af3e194161fbadcc2151d3a058e8" @@ -3224,11 +3171,6 @@ indent-string@^4.0.0: resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251" integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg== -inflection@^1.12.0: - version "1.12.0" - resolved "https://registry.yarnpkg.com/inflection/-/inflection-1.12.0.tgz#a200935656d6f5f6bc4dc7502e1aecb703228416" - integrity sha1-ogCTVlbW9fa8TcdQLhrstwMihBY= - inflight@^1.0.4: version "1.0.6" resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" @@ -4874,6 +4816,11 @@ qs@^6.5.1: resolved "https://registry.yarnpkg.com/qs/-/qs-6.9.1.tgz#20082c65cb78223635ab1a9eaca8875a29bf8ec9" integrity sha512-Cxm7/SS/y/Z3MHWSxXb8lIFqgqBowP5JMlTUFyJN88y0SGQhVmZnqFK/PeuMX9LzUyWsqqhNxIyg0jlzq946yA== +ramda@0.26.1: + version "0.26.1" + resolved "https://registry.yarnpkg.com/ramda/-/ramda-0.26.1.tgz#8d41351eb8111c55353617fc3bbffad8e4d35d06" + integrity sha512-hLWjpy7EnsDBb0p+Z3B7rPi3GDeRG5ZtiI33kJhTt+ORCd38AbAIjB/9zRIUoeTbE/AVX5ZkU7m6bznsvrf8eQ== + range-parser@~1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" @@ -5206,7 +5153,7 @@ semver@7.0.0: resolved "https://registry.yarnpkg.com/semver/-/semver-7.0.0.tgz#5f3ca35761e47e05b206c6daff2cf814f0316b8e" integrity sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A== -semver@^5.0.3, semver@^5.1.0, semver@^5.3.0, semver@^5.4.1, semver@^5.5.0, semver@^5.6.0, semver@^5.7.0, semver@^5.7.1: +semver@^5.0.3, semver@^5.1.0, semver@^5.4.1, semver@^5.5.0, semver@^5.6.0, semver@^5.7.0, semver@^5.7.1: version "5.7.1" resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== @@ -5300,11 +5247,6 @@ shebang-regex@^3.0.0: resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== -shimmer@^1.1.0, shimmer@^1.2.0: - version "1.2.1" - resolved "https://registry.yarnpkg.com/shimmer/-/shimmer-1.2.1.tgz#610859f7de327b587efebf501fb43117f9aff337" - integrity sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw== - side-channel@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.2.tgz#df5d1abadb4e4bf4af1cd8852bf132d2f7876947" @@ -6005,6 +5947,11 @@ utils-merge@1.0.1: resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM= +uuid@3.3.2: + version "3.3.2" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131" + integrity sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA== + uuid@^3.3.2, uuid@^3.3.3: version "3.4.0" resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" From e167c6c6eb762cc4cfff30491b3284ddc31d55e2 Mon Sep 17 00:00:00 2001 From: Robus Gauli Date: Fri, 10 Apr 2020 14:15:56 +0545 Subject: [PATCH 16/24] Remove session.js used for continuation-local-storage --- src/auth/session.js | 14 -------------- 1 file changed, 14 deletions(-) delete mode 100644 src/auth/session.js diff --git a/src/auth/session.js b/src/auth/session.js deleted file mode 100644 index 62ddb13..0000000 --- a/src/auth/session.js +++ /dev/null @@ -1,14 +0,0 @@ -import { createNamespace, getNamespace } from 'continuation-local-storage'; - -/** - * User session namespace limited to request response roundtrip. - */ -export const userSession = createNamespace('user-namespace'); - -/** - * User object that is consumed by services. - */ -const session = getNamespace('user-namespace'); -const user = () => session.get('user'); - -export default user; From d92508c8b5d0d2d96c24732ca6962244c94ba9cf Mon Sep 17 00:00:00 2001 From: Robus Gauli Date: Fri, 10 Apr 2020 17:13:47 +0545 Subject: [PATCH 17/24] Allow pagination for fetch query --- package.json | 2 +- src/auth/index.js | 12 +++-- src/constants/db.js | 1 - src/controllers/user.js | 10 ++-- .../20170107202211_create_users_table.js | 2 +- src/models/model.js | 47 +++++++++---------- src/models/user.js | 9 ++-- src/routes/user.js | 2 +- src/seeds/01_insert_users.js | 2 +- src/services/user.js | 11 ++--- src/utils/math.js | 10 ++++ 11 files changed, 57 insertions(+), 51 deletions(-) delete mode 100644 src/constants/db.js create mode 100644 src/utils/math.js diff --git a/package.json b/package.json index 3d4a203..62eba91 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "starter", + "name": "@leapfrogtechnology/nodejs-starter", "version": "1.0.0", "description": "NodeJS Starter", "scripts": { diff --git a/src/auth/index.js b/src/auth/index.js index cf52a3d..007737a 100644 --- a/src/auth/index.js +++ b/src/auth/index.js @@ -6,28 +6,30 @@ import TokenError from '../errors/token'; /** * Get token from header in http request. * - * @param {Object} req + * @param {Object} req + * + * @returns {Object} */ function getTokenFromHeaders(req) { const { headers: { authorization = '' }, } = req; - const fields = authorization.split(' ').filter(Boolean); + const [tokenType, token] = authorization.split(' ').filter(Boolean); - if (fields.length <= 1 || fields[0] !== 'Bearer') { + if (tokenType !== 'Bearer' || !token) { return { ok: false, }; } return { - token: fields[1], + token, }; } /** - * Fetch user from auth server from token. + * Fetch user from auth server using token. * * @param {String} token * @throws NetworkError diff --git a/src/constants/db.js b/src/constants/db.js deleted file mode 100644 index b4336e9..0000000 --- a/src/constants/db.js +++ /dev/null @@ -1 +0,0 @@ -export const DB_DUPLICATE_ENTRY = 'ER_DUP_ENTRY'; diff --git a/src/controllers/user.js b/src/controllers/user.js index 07b0163..efeeedc 100644 --- a/src/controllers/user.js +++ b/src/controllers/user.js @@ -1,7 +1,5 @@ import HttpStatus from 'http-status-codes'; -import logger from '../utils/logger'; - import * as userService from '../services/user'; /** @@ -11,13 +9,12 @@ import * as userService from '../services/user'; * @param {Object} res * @param {Function} next */ -export async function fetchAll(req, res, next) { +export async function fetch(req, res, next) { try { - const data = await userService.fetchAll(); + const data = await userService.fetch(); res.json({ data }); } catch (err) { - logger.error(err); next(err); } } @@ -31,11 +28,10 @@ export async function fetchAll(req, res, next) { */ export async function create(req, res, next) { try { - const data = await userService.create(req.body); + const data = await userService.create(); res.status(HttpStatus.CREATED).json({ data }); } catch (err) { - logger.error(err); next(err); } } diff --git a/src/migrations/20170107202211_create_users_table.js b/src/migrations/20170107202211_create_users_table.js index 11eeddc..d6977a7 100644 --- a/src/migrations/20170107202211_create_users_table.js +++ b/src/migrations/20170107202211_create_users_table.js @@ -7,7 +7,7 @@ export function up(knex) { return knex.schema.createTable('users', (table) => { table.increments(); - table.string('name').default('defualt name'); + table.string('name'); }); } diff --git a/src/models/model.js b/src/models/model.js index d022651..426f739 100644 --- a/src/models/model.js +++ b/src/models/model.js @@ -1,15 +1,11 @@ import db from '../db'; +import { clamp } from '../utils/math'; + /** - * @class Model - * * Base class that is extended by domain models such as users, leave, etc. */ class Model { - constructor() { - this._db = db(this.getTable()); - } - /** * This method is required by the domain class. * @@ -21,32 +17,33 @@ class Model { /** * This method persists the payload object to underlying database. + * NOTE: Rollback triggers with rejected promise. * - * @param {Object} payload + * @param {Object} payload * * @returns {Promise} */ save(payload = {}) { - return new Promise((resolve, reject) => { - db.transaction((trx) => { - this._db - .transacting(trx) - .insert(payload) - .then((response) => { - trx.commit(response); - }) - .catch((err) => { - trx.rollback(err); - }); - }) - .then((response) => { - resolve(response); - }) - .catch((err) => { - reject(err); - }); + return db.transaction((trx) => { + return db(this.getTable()).transacting(trx).insert(payload); }); } + + /** + * This method fetches rows from database provided offset and limit. + * + * @param {Object} param0 + * + * @returns {Promise} + */ + fetch({ offset, limit }) { + // Clamp the limit of the pagination to 100 exclusive + limit = clamp(limit, 0, 100); + // Only positive offset allowed + offset = Math.max(0, offset); + + return db(this.getTable()).limit(limit).offset(offset); + } } export default Model; diff --git a/src/models/user.js b/src/models/user.js index 842ae5c..3897d5b 100644 --- a/src/models/user.js +++ b/src/models/user.js @@ -1,11 +1,14 @@ import Model from './model'; /** - * @class User - * - * User model for basic representing user entity. + * User model representing user entity. */ class User extends Model { + /** + * Returns table name associated with User model. + * + * @returns {String} + */ getTable() { return 'users'; } diff --git a/src/routes/user.js b/src/routes/user.js index bc587c4..9df2e41 100644 --- a/src/routes/user.js +++ b/src/routes/user.js @@ -7,7 +7,7 @@ const router = Router(); /** * GET /api/users */ -router.get('/', userController.fetchAll); +router.get('/', userController.fetch); /** * POST /api/users diff --git a/src/seeds/01_insert_users.js b/src/seeds/01_insert_users.js index 32ee6fe..b9351aa 100644 --- a/src/seeds/01_insert_users.js +++ b/src/seeds/01_insert_users.js @@ -10,7 +10,7 @@ export function seed(knex) { .then(() => { return knex('users').insert([ { - name: 'Sample user', + name: 'John Doe', updated_at: new Date(), }, ]); diff --git a/src/services/user.js b/src/services/user.js index bf9bbdd..b13882b 100644 --- a/src/services/user.js +++ b/src/services/user.js @@ -8,10 +8,9 @@ import logger from '../utils/logger'; * * @returns {Promise} */ -export async function fetchAll() { - const users = await User.fetchAll(); - - return users; +export function fetch() { + // Example: should retrieve from the query parameter + return User.fetch({ limit: 3, offset: 4 }); } /** @@ -23,11 +22,11 @@ export async function create() { // Example for retrieving the user from async-store in service. const user = store.get('user'); - const id = await User.save(user); + const [id] = await User.save(user); logger.info(`User created: ${user}`); return { - id: id.pop(), + id, }; } diff --git a/src/utils/math.js b/src/utils/math.js new file mode 100644 index 0000000..821ab4f --- /dev/null +++ b/src/utils/math.js @@ -0,0 +1,10 @@ +/** + * Clamps value between min and max. + * + * @param {Number} value + * @param {Number} min + * @param {Number} max + */ +export function clamp(value, min, max) { + return Math.min(Math.max(value, min), max); +} From 6a46a5d43823a9545005792adf5dae62d610d7b6 Mon Sep 17 00:00:00 2001 From: Robus Gauli Date: Fri, 10 Apr 2020 17:26:20 +0545 Subject: [PATCH 18/24] Remove unused joi validation service --- src/validators/userValidator.js | 20 ++------------------ 1 file changed, 2 insertions(+), 18 deletions(-) diff --git a/src/validators/userValidator.js b/src/validators/userValidator.js index b8fa3c8..1457756 100644 --- a/src/validators/userValidator.js +++ b/src/validators/userValidator.js @@ -1,7 +1,6 @@ import Joi from '@hapi/joi'; import validate from '../utils/validate'; -import * as userService from '../services/userService'; // Validation schema const schema = Joi.object({ @@ -19,22 +18,7 @@ const schema = Joi.object({ function userValidator(req, res, next) { return validate(req.body, schema) .then(() => next()) - .catch((err) => next(err)); + .catch(next); } -/** - * Validate users existence. - * - * @param {Object} req - * @param {Object} res - * @param {Function} next - * @returns {Promise} - */ -function findUser(req, res, next) { - return userService - .getUser(req.params.id) - .then(() => next()) - .catch((err) => next(err)); -} - -export { findUser, userValidator }; +export { userValidator }; From 2566319431fe2bda84a7b90f7b64a9557ff71c41 Mon Sep 17 00:00:00 2001 From: Robus Gauli Date: Fri, 10 Apr 2020 18:32:05 +0545 Subject: [PATCH 19/24] Clean up log format --- src/utils/logger.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/utils/logger.js b/src/utils/logger.js index fd853d7..c67d59a 100644 --- a/src/utils/logger.js +++ b/src/utils/logger.js @@ -28,9 +28,9 @@ const logFormat = format.printf((info) => { // TODO: Will there be a situation when requestID would be empty string? // May logs before middleware initialization? const requestID = store.getShortId(); - const formattedReqID = requestID ? `[${requestID}] ` : ''; + const formattedReqID = requestID ? `[${requestID}]` : ''; - return `${info.timestamp} [${info.level}] [${info.label}] ${formattedReqID}${formattedNamespace}: ${info.message}`; + return `${info.timestamp} [${info.level}] [${info.label}] ${formattedReqID} ${formattedNamespace}: ${info.message}`; }); /** From a459ca895191f1483c1c04611eb51f3507f58948 Mon Sep 17 00:00:00 2001 From: Robus Gauli Date: Sun, 12 Apr 2020 19:40:35 +0545 Subject: [PATCH 20/24] Add prettier rule for strpping single arg braces --- .prettierrc | 1 + src/index.js | 4 ++-- .../20170107202211_create_users_table.js | 2 +- src/models/model.js | 2 +- src/utils/buildError.js | 2 +- src/utils/http.js | 2 +- src/utils/logger.js | 2 +- test/api.test.js | 4 ++-- test/controllers/users.test.js | 22 +++++++++---------- 9 files changed, 21 insertions(+), 20 deletions(-) diff --git a/.prettierrc b/.prettierrc index 8d7bf5f..57833c4 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,3 +1,4 @@ tabWidth: 2 printWidth: 120 singleQuote: true +arrowParens: avoid \ No newline at end of file diff --git a/src/index.js b/src/index.js index f9078c9..c641f50 100644 --- a/src/index.js +++ b/src/index.js @@ -78,7 +78,7 @@ app.listen(app.get('port'), app.get('host'), () => { }); // Catch unhandled rejections -process.on('unhandledRejection', (err) => { +process.on('unhandledRejection', err => { logger.error('Unhandled rejection', err); try { @@ -91,7 +91,7 @@ process.on('unhandledRejection', (err) => { }); // Catch uncaught exceptions -process.on('uncaughtException', (err) => { +process.on('uncaughtException', err => { logger.error('Uncaught exception', err); try { diff --git a/src/migrations/20170107202211_create_users_table.js b/src/migrations/20170107202211_create_users_table.js index d6977a7..b2753da 100644 --- a/src/migrations/20170107202211_create_users_table.js +++ b/src/migrations/20170107202211_create_users_table.js @@ -5,7 +5,7 @@ * @returns {Promise} */ export function up(knex) { - return knex.schema.createTable('users', (table) => { + return knex.schema.createTable('users', table => { table.increments(); table.string('name'); }); diff --git a/src/models/model.js b/src/models/model.js index 426f739..11494d8 100644 --- a/src/models/model.js +++ b/src/models/model.js @@ -24,7 +24,7 @@ class Model { * @returns {Promise} */ save(payload = {}) { - return db.transaction((trx) => { + return db.transaction(trx => { return db(this.getTable()).transacting(trx).insert(payload); }); } diff --git a/src/utils/buildError.js b/src/utils/buildError.js index be59172..b8870e1 100644 --- a/src/utils/buildError.js +++ b/src/utils/buildError.js @@ -18,7 +18,7 @@ function buildError(err) { message: HttpStatus.getStatusText(HttpStatus.BAD_REQUEST), details: err.details && - err.details.map((err) => { + err.details.map(err => { return { message: err.message, param: err.path.join('.'), diff --git a/src/utils/http.js b/src/utils/http.js index b12d7cc..989cac2 100644 --- a/src/utils/http.js +++ b/src/utils/http.js @@ -9,7 +9,7 @@ import NetworkError from '../errors/network'; const http = axios.create(); http.interceptors.request.use( - (config) => { + config => { return config; }, function (error) { diff --git a/src/utils/logger.js b/src/utils/logger.js index c67d59a..3a7d4ae 100644 --- a/src/utils/logger.js +++ b/src/utils/logger.js @@ -18,7 +18,7 @@ if (!fs.existsSync(LOG_DIR)) { } // logFormat used for console logging -const logFormat = format.printf((info) => { +const logFormat = format.printf(info => { let formattedNamespace = ''; if (info.metadata.namespace) { diff --git a/test/api.test.js b/test/api.test.js index 86674a8..4893b2c 100644 --- a/test/api.test.js +++ b/test/api.test.js @@ -3,7 +3,7 @@ import app from '../src/index'; import request from 'supertest'; describe('Base API Test', () => { - it('should return API version and title for the app', (done) => { + it('should return API version and title for the app', done => { request(app) .get('/api') .end((err, res) => { @@ -15,7 +15,7 @@ describe('Base API Test', () => { }); }); - it('should return 405 method not allowed for random API hits', (done) => { + it('should return 405 method not allowed for random API hits', done => { const randomString = Math.random().toString(36).substr(2, 5); request(app) diff --git a/test/controllers/users.test.js b/test/controllers/users.test.js index 5a6c9f7..3fe8e3d 100644 --- a/test/controllers/users.test.js +++ b/test/controllers/users.test.js @@ -7,14 +7,14 @@ import bookshelf from '../../src/db'; * Tests for '/api/users' */ describe('Users Controller Test', () => { - before((done) => { + before(done => { bookshelf .knex('users') .truncate() .then(() => done()); }); - it('should return list of users', (done) => { + it('should return list of users', done => { request(app) .get('/api/users') .end((err, res) => { @@ -26,7 +26,7 @@ describe('Users Controller Test', () => { }); }); - it('should not create a new user if name is not provided', (done) => { + it('should not create a new user if name is not provided', done => { const user = { noname: 'Jane Doe', }; @@ -48,7 +48,7 @@ describe('Users Controller Test', () => { }); }); - it('should create a new user with valid data', (done) => { + it('should create a new user with valid data', done => { const user = { name: 'Jane Doe', }; @@ -71,7 +71,7 @@ describe('Users Controller Test', () => { }); }); - it('should get information of user', (done) => { + it('should get information of user', done => { request(app) .get('/api/users/1') .end((err, res) => { @@ -88,7 +88,7 @@ describe('Users Controller Test', () => { }); }); - it('should respond with not found error if random user id is provided', (done) => { + it('should respond with not found error if random user id is provided', done => { request(app) .get('/api/users/1991') .end((err, res) => { @@ -102,7 +102,7 @@ describe('Users Controller Test', () => { }); }); - it('should update a user if name is provided', (done) => { + it('should update a user if name is provided', done => { const user = { name: 'John Doe', }; @@ -124,7 +124,7 @@ describe('Users Controller Test', () => { }); }); - it('should not update a user if name is not provided', (done) => { + it('should not update a user if name is not provided', done => { const user = { noname: 'John Doe', }; @@ -146,7 +146,7 @@ describe('Users Controller Test', () => { }); }); - it('should delete a user if valid id is provided', (done) => { + it('should delete a user if valid id is provided', done => { request(app) .delete('/api/users/1') .end((err, res) => { @@ -156,7 +156,7 @@ describe('Users Controller Test', () => { }); }); - it('should respond with not found error if random user id is provided for deletion', (done) => { + it('should respond with not found error if random user id is provided for deletion', done => { request(app) .delete('/api/users/1991') .end((err, res) => { @@ -170,7 +170,7 @@ describe('Users Controller Test', () => { }); }); - it('should respond with bad request for empty JSON in request body', (done) => { + it('should respond with bad request for empty JSON in request body', done => { const user = {}; request(app) From 1b9a6b2917ae25d60ae86416f6d11473725ab1b2 Mon Sep 17 00:00:00 2001 From: Robus Gauli Date: Sun, 12 Apr 2020 19:49:25 +0545 Subject: [PATCH 21/24] Rename env flag for file transport --- .env.docker | 5 +++-- .env.example | 3 ++- src/utils/logger.js | 4 ++-- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/.env.docker b/.env.docker index 0ec71b8..1332420 100644 --- a/.env.docker +++ b/.env.docker @@ -5,8 +5,9 @@ APP_PORT='8848' APP_HOST='0.0.0.0' # Log -LOGGING_DIR='logs' -LOGGING_LEVEL='debug' +LOG_DIR='logs' +LOG_LEVEL='debug' +ENABLE_FILE_LOG_TRANSPORT='TRUE' # Database DB_CLIENT='pg' diff --git a/.env.example b/.env.example index 2e9c559..cd16f54 100644 --- a/.env.example +++ b/.env.example @@ -5,8 +5,9 @@ APP_PORT='8848' APP_HOST='127.0.0.1' # Log +LOG_DIR='logs' LOG_LEVEL='debug' -ENABLE_FILE_LOG='TRUE' +ENABLE_FILE_LOG_TRANSPORT='TRUE' # Database DB_CLIENT='mysql' diff --git a/src/utils/logger.js b/src/utils/logger.js index 3a7d4ae..72c455c 100644 --- a/src/utils/logger.js +++ b/src/utils/logger.js @@ -10,7 +10,7 @@ import 'winston-daily-rotate-file'; const LOG_DIR = process.env.LOG_DIR || 'logs'; const LOG_LEVEL = process.env.LOG_LEVEL || 'info'; -const isFileLogEnabled = process.env.ENABLE_FILE_LOG === 'TRUE'; +const isFileLogTransportEnabled = process.env.ENABLE_FILE_LOG_TRANSPORT === 'TRUE'; // Create log directory if it does not exist if (!fs.existsSync(LOG_DIR)) { @@ -72,7 +72,7 @@ function setupTransports() { level: 'info', }) ); - if (isFileLogEnabled) { + if (isFileLogTransportEnabled) { transports.push( new winston.transports.DailyRotateFile({ format: format.combine(format.timestamp(), format.json()), From 4dc44ec3b6761857707f0dfa069e08623ec23db1 Mon Sep 17 00:00:00 2001 From: Robus Gauli Date: Sun, 12 Apr 2020 20:14:49 +0545 Subject: [PATCH 22/24] Remove http error codes from CustomError --- .env.docker | 2 +- .env.example | 4 ++-- .travis.yml | 2 +- package.json | 1 + src/auth/index.js | 16 ++++++---------- src/errors/error.js | 12 ------------ src/errors/token.js | 11 ----------- src/utils/buildError.js | 14 ++++++++++++-- 8 files changed, 23 insertions(+), 39 deletions(-) diff --git a/.env.docker b/.env.docker index 1332420..425ee2d 100644 --- a/.env.docker +++ b/.env.docker @@ -1,5 +1,5 @@ # Application -APP_NAME='Express API ES6 Starter' +APP_NAME='Node JS Starter' APP_VERSION='1.0.0' APP_PORT='8848' APP_HOST='0.0.0.0' diff --git a/.env.example b/.env.example index cd16f54..cb13c9f 100644 --- a/.env.example +++ b/.env.example @@ -1,5 +1,5 @@ # Application -APP_NAME='Express API ES6 Starter' +APP_NAME='Node JS Starter' APP_VERSION='1.0.0' APP_PORT='8848' APP_HOST='127.0.0.1' @@ -22,7 +22,7 @@ DB_PASSWORD='password' # Authenication parameters -AUTH_URL='localhost:5000/' +AUTH_URL='http://localhost:5000/' AUTH_CLIENT_ID='secret-client-id' # Sentry diff --git a/.travis.yml b/.travis.yml index 69c416d..7cc5cab 100644 --- a/.travis.yml +++ b/.travis.yml @@ -14,7 +14,7 @@ branches: env: > NODE_ENV=test - APP_NAME='Node JS starter' + APP_NAME='Node JS Starter' APP_VERSION='1.0.0' TEST_APP_PORT='9945' TEST_DB_NAME='app_test' diff --git a/package.json b/package.json index 62eba91..6b5dcd6 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "api" ], "private": true, + "author": "Saugat Acharya ", "license": "MIT", "dependencies": { "@hapi/boom": "^9.1.0", diff --git a/src/auth/index.js b/src/auth/index.js index 007737a..bc7f490 100644 --- a/src/auth/index.js +++ b/src/auth/index.js @@ -4,16 +4,13 @@ import { http } from '../utils/http'; import TokenError from '../errors/token'; /** - * Get token from header in http request. - * - * @param {Object} req + * Extract token from headers in http request. * + * @param {Object} headers * @returns {Object} */ -function getTokenFromHeaders(req) { - const { - headers: { authorization = '' }, - } = req; +function extractTokenFromHeaders(headers = {}) { + const { authorization = '' } = headers; const [tokenType, token] = authorization.split(' ').filter(Boolean); @@ -32,8 +29,7 @@ function getTokenFromHeaders(req) { * Fetch user from auth server using token. * * @param {String} token - * @throws NetworkError - * + * @throws {NetworkError} * @returns {Promise} */ async function fetchUserByToken(token) { @@ -56,7 +52,7 @@ async function fetchUserByToken(token) { */ async function authenticateUser(req, res, next) { try { - const { ok, token } = getTokenFromHeaders(req); + const { ok, token } = extractTokenFromHeaders(req.headers); if (!ok) { throw new TokenError('Invalid token'); diff --git a/src/errors/error.js b/src/errors/error.js index df4f893..bc060bc 100644 --- a/src/errors/error.js +++ b/src/errors/error.js @@ -1,5 +1,3 @@ -import HttpStatus from 'http-status-codes'; - /** * Base class for error. */ @@ -8,7 +6,6 @@ class BaseError extends Error { * Constructor method for BaseError. * * @param {String} message - * * @returns {BaseError} */ constructor(message = '') { @@ -16,15 +13,6 @@ class BaseError extends Error { // This flag is used to distinguished from other error types such as joi, boom, etc. this.isCustom = true; } - - /** - * Generic http status code for custom errors. - * - * @returns {Number} - */ - httpCode() { - return HttpStatus.INTERNAL_SERVER_ERROR; - } } export default BaseError; diff --git a/src/errors/token.js b/src/errors/token.js index d27146e..bb78a9c 100644 --- a/src/errors/token.js +++ b/src/errors/token.js @@ -1,5 +1,3 @@ -import HttpStatus from 'http-status-codes'; - import BaseError from './error'; /** @@ -14,15 +12,6 @@ class TokenError extends BaseError { toString() { return `Token Error: ${this.message}`; } - - /** - * Returns http status code for invalid token. - * - * @returns {Number} - */ - httpCode() { - return HttpStatus.UNAUTHORIZED; - } } export default TokenError; diff --git a/src/utils/buildError.js b/src/utils/buildError.js index b8870e1..ecc3afc 100644 --- a/src/utils/buildError.js +++ b/src/utils/buildError.js @@ -1,6 +1,8 @@ import HttpStatus from 'http-status-codes'; import * as store from '@leapfrogtechnology/async-store'; +import TokenError from '../errors/token'; + /** * Build error response for validation errors. * @@ -38,10 +40,18 @@ function buildError(err) { // Custom errors if (err.isCustom) { + if (err instanceof TokenError) { + return { + id: requestID, + code: HttpStatus.UNAUTHORIZED, + message: HttpStatus.getStatusText(HttpStatus.UNAUTHORIZED), + }; + } + return { id: requestID, - code: err.httpCode(), - message: err.message, + code: HttpStatus.INTERNAL_SERVER_ERROR, + message: err.message || HttpStatus.getStatusText(HttpStatus.INTERNAL_SERVER_ERROR), }; } From 27c155aa237dcb1b386ad2fc0f524fd107d59d27 Mon Sep 17 00:00:00 2001 From: Robus Gauli Date: Tue, 14 Apr 2020 08:28:55 +0545 Subject: [PATCH 23/24] Remove useless request interceptor --- src/auth/index.js | 2 ++ src/index.js | 9 +++++---- src/middlewares/errorHandler.js | 1 - src/models/model.js | 8 +++----- src/routes.js | 30 +++++++++++++++++------------- src/utils/http.js | 20 -------------------- src/utils/logger.js | 1 - 7 files changed, 27 insertions(+), 44 deletions(-) diff --git a/src/auth/index.js b/src/auth/index.js index bc7f490..26d1df0 100644 --- a/src/auth/index.js +++ b/src/auth/index.js @@ -2,6 +2,7 @@ import * as store from '@leapfrogtechnology/async-store'; import { http } from '../utils/http'; import TokenError from '../errors/token'; +import logger from '../utils/logger'; /** * Extract token from headers in http request. @@ -63,6 +64,7 @@ async function authenticateUser(req, res, next) { store.set(user); next(); } catch (err) { + logger.error(err); next(err); } } diff --git a/src/index.js b/src/index.js index c641f50..242f99d 100644 --- a/src/index.js +++ b/src/index.js @@ -13,9 +13,9 @@ import compression from 'compression'; import * as Sentry from '@sentry/node'; import * as store from '@leapfrogtechnology/async-store'; -import routes from './routes'; import json from './middlewares/json'; import logger, { logStream } from './utils/logger'; +import { publicRouter, privateRouter } from './routes'; import * as errorHandler from './middlewares/errorHandler'; // Initialize Sentry @@ -52,7 +52,8 @@ app.use(errorHandler.bodyParser); app.use(json); // API Routes -app.use('/api', routes); +app.use('/api', publicRouter); +app.use('/api', privateRouter); // Swagger UI // Workaround for changing the default URL in swagger.json @@ -62,8 +63,8 @@ const swaggerIndexContent = fs .toString() .replace('https://petstore.swagger.io/v2/swagger.json', '/api/swagger.json'); -app.get('/api-docs/index.html', (req, res) => res.send(swaggerIndexContent)); -app.get('/api-docs', (req, res) => res.redirect('/api-docs/index.html')); +app.get('/api-docs/index.html', (_, res) => res.send(swaggerIndexContent)); +app.get('/api-docs', (_, res) => res.redirect('/api-docs/index.html')); app.use('/api-docs', express.static(pathToSwaggerUi)); // This error handler must be before any other error middleware diff --git a/src/middlewares/errorHandler.js b/src/middlewares/errorHandler.js index a348359..d255b2b 100644 --- a/src/middlewares/errorHandler.js +++ b/src/middlewares/errorHandler.js @@ -63,7 +63,6 @@ export function bodyParser(err, req, res, next) { * @param {Function} next */ export function genericErrorHandler(err, req, res, next) { - logger.error(err.stack); const error = buildError(err); res.status(error.code).json({ error }); diff --git a/src/models/model.js b/src/models/model.js index 11494d8..c97f8a1 100644 --- a/src/models/model.js +++ b/src/models/model.js @@ -3,7 +3,7 @@ import db from '../db'; import { clamp } from '../utils/math'; /** - * Base class that is extended by domain models such as users, leave, etc. + * Base class that is extended by domain models. */ class Model { /** @@ -19,8 +19,7 @@ class Model { * This method persists the payload object to underlying database. * NOTE: Rollback triggers with rejected promise. * - * @param {Object} payload - * + * @param {Object} payload * @returns {Promise} */ save(payload = {}) { @@ -32,8 +31,7 @@ class Model { /** * This method fetches rows from database provided offset and limit. * - * @param {Object} param0 - * + * @param {Object} param0 * @returns {Promise} */ fetch({ offset, limit }) { diff --git a/src/routes.js b/src/routes.js index 3699b3c..ff06474 100644 --- a/src/routes.js +++ b/src/routes.js @@ -1,37 +1,41 @@ import { Router } from 'express'; import authenticateUser from './auth'; - -import swaggerSpec from './utils/swagger'; import userRoutes from './routes/user'; +import swaggerSpec from './utils/swagger'; /** - * Contains all API routes for the application. + * Contains public API routes for the application. */ -const router = Router(); +const publicRouter = Router(); /** * GET /api/swagger.json */ -router.get('/swagger.json', (req, res) => { +publicRouter.get('/swagger.json', (_, res) => { res.json(swaggerSpec); }); -/** - * LMS Authentication middleware - */ -router.use(authenticateUser); - /** * GET /api */ -router.get('/', (req, res) => { +publicRouter.get('/', (req, res) => { res.json({ app: req.app.locals.title, apiVersion: req.app.locals.version, }); }); -router.use('/users', userRoutes); +/** + * Contains secured API routes for the application. + */ +const privateRouter = Router(); + +/** + * Authentication middleware for private routes. + */ +privateRouter.use(authenticateUser); + +privateRouter.use('/users', userRoutes); -export default router; +export { publicRouter, privateRouter }; diff --git a/src/utils/http.js b/src/utils/http.js index 989cac2..1cd9956 100644 --- a/src/utils/http.js +++ b/src/utils/http.js @@ -1,28 +1,8 @@ import axios from 'axios'; -import HttpStatus from 'http-status-codes'; - -import NetworkError from '../errors/network'; /** * Axios Object */ const http = axios.create(); -http.interceptors.request.use( - config => { - return config; - }, - function (error) { - if (error.response) { - return Promise.reject(error); - } else { - return Promise.reject( - new NetworkError({ - code: HttpStatus.INTERNAL_SERVER_ERROR, - }) - ); - } - } -); - export { http }; diff --git a/src/utils/logger.js b/src/utils/logger.js index 72c455c..dc0503f 100644 --- a/src/utils/logger.js +++ b/src/utils/logger.js @@ -51,7 +51,6 @@ const logger = winston.createLogger({ * Creates a child logger with namespace for logging. * * @param {String} namespace - * * @returns {Object} */ logger.withNamespace = function (namespace) { From abf5472b448f43a1841c924740af453ec3c71ce5 Mon Sep 17 00:00:00 2001 From: Robus Gauli Date: Tue, 14 Apr 2020 09:31:06 +0545 Subject: [PATCH 24/24] Fix pr issues --- .env.docker | 3 ++- .env.example | 1 + .prettierrc | 2 +- src/auth/index.js | 2 -- src/errors/database.js | 11 +++++++++++ src/errors/network.js | 11 +++++++++++ src/errors/token.js | 11 +++++++++++ src/middlewares/errorHandler.js | 1 + src/models/model.js | 8 ++++---- src/utils/logger.js | 12 ++++-------- 10 files changed, 46 insertions(+), 16 deletions(-) diff --git a/.env.docker b/.env.docker index 425ee2d..5dffa0a 100644 --- a/.env.docker +++ b/.env.docker @@ -7,10 +7,11 @@ APP_HOST='0.0.0.0' # Log LOG_DIR='logs' LOG_LEVEL='debug' +LOG_RETENTION_PERIOD='' ENABLE_FILE_LOG_TRANSPORT='TRUE' # Database -DB_CLIENT='pg' +DB_CLIENT='mysql' # App Environment DB_PORT='5432' diff --git a/.env.example b/.env.example index cb13c9f..975553d 100644 --- a/.env.example +++ b/.env.example @@ -7,6 +7,7 @@ APP_HOST='127.0.0.1' # Log LOG_DIR='logs' LOG_LEVEL='debug' +LOG_RETENTION_PERIOD='' ENABLE_FILE_LOG_TRANSPORT='TRUE' # Database diff --git a/.prettierrc b/.prettierrc index 57833c4..4ecea61 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,4 +1,4 @@ tabWidth: 2 printWidth: 120 singleQuote: true -arrowParens: avoid \ No newline at end of file +arrowParens: avoid diff --git a/src/auth/index.js b/src/auth/index.js index 26d1df0..bc7f490 100644 --- a/src/auth/index.js +++ b/src/auth/index.js @@ -2,7 +2,6 @@ import * as store from '@leapfrogtechnology/async-store'; import { http } from '../utils/http'; import TokenError from '../errors/token'; -import logger from '../utils/logger'; /** * Extract token from headers in http request. @@ -64,7 +63,6 @@ async function authenticateUser(req, res, next) { store.set(user); next(); } catch (err) { - logger.error(err); next(err); } } diff --git a/src/errors/database.js b/src/errors/database.js index 1ef7c07..42af01d 100644 --- a/src/errors/database.js +++ b/src/errors/database.js @@ -4,6 +4,17 @@ import BaseError from './error'; * Error class for database failure and error. */ class DatabaseError extends BaseError { + /** + * Constructor for DatabaseError. + * + * @param {String} message + * @returns {DatabaseError} + */ + constructor(message) { + super(message); + this.name = 'DatabaseError'; + } + /** * Returns the formatted string representation of error. * diff --git a/src/errors/network.js b/src/errors/network.js index b37ffbc..1adb1b3 100644 --- a/src/errors/network.js +++ b/src/errors/network.js @@ -4,6 +4,17 @@ import BaseError from './error'; * Error class for Network error. */ class NetworkError extends BaseError { + /** + * Constructor for NetworkError. + * + * @param {String} message + * @returns {NetworkError} + */ + constructor(message) { + super(message); + this.name = 'NetworkError'; + } + /** * Returns the formatted string representation of error. * diff --git a/src/errors/token.js b/src/errors/token.js index bb78a9c..f0d36ed 100644 --- a/src/errors/token.js +++ b/src/errors/token.js @@ -4,6 +4,17 @@ import BaseError from './error'; * Error class for Token Error. */ class TokenError extends BaseError { + /** + * Constructor for TokenError. + * + * @param {String} message + * @returns {TokenError} + */ + constructor(message) { + super(message); + this.name = 'TokenError'; + } + /** * Returns the formatted string representation of error. * diff --git a/src/middlewares/errorHandler.js b/src/middlewares/errorHandler.js index d255b2b..a348359 100644 --- a/src/middlewares/errorHandler.js +++ b/src/middlewares/errorHandler.js @@ -63,6 +63,7 @@ export function bodyParser(err, req, res, next) { * @param {Function} next */ export function genericErrorHandler(err, req, res, next) { + logger.error(err.stack); const error = buildError(err); res.status(error.code).json({ error }); diff --git a/src/models/model.js b/src/models/model.js index c97f8a1..8ff1436 100644 --- a/src/models/model.js +++ b/src/models/model.js @@ -31,14 +31,14 @@ class Model { /** * This method fetches rows from database provided offset and limit. * - * @param {Object} param0 + * @param {Object} params * @returns {Promise} */ - fetch({ offset, limit }) { + fetch(params) { // Clamp the limit of the pagination to 100 exclusive - limit = clamp(limit, 0, 100); + const limit = clamp(params.limit, 0, 100); // Only positive offset allowed - offset = Math.max(0, offset); + const offset = Math.max(0, params.offset); return db(this.getTable()).limit(limit).offset(offset); } diff --git a/src/utils/logger.js b/src/utils/logger.js index dc0503f..c872fe3 100644 --- a/src/utils/logger.js +++ b/src/utils/logger.js @@ -19,18 +19,14 @@ if (!fs.existsSync(LOG_DIR)) { // logFormat used for console logging const logFormat = format.printf(info => { - let formattedNamespace = ''; - - if (info.metadata.namespace) { - formattedNamespace = `[${info.metadata.namespace}]`; - } + const formattedNamespace = info.metadata.namespace ? info.metadata.namespace : ''; // TODO: Will there be a situation when requestID would be empty string? // May logs before middleware initialization? const requestID = store.getShortId(); - const formattedReqID = requestID ? `[${requestID}]` : ''; + const formattedReqID = requestID ? requestID : ''; - return `${info.timestamp} [${info.level}] [${info.label}] ${formattedReqID} ${formattedNamespace}: ${info.message}`; + return `${info.timestamp} [${info.level}] [${info.label}] [${formattedReqID}] [${formattedNamespace}]: ${info.message}`; }); /** @@ -75,7 +71,7 @@ function setupTransports() { transports.push( new winston.transports.DailyRotateFile({ format: format.combine(format.timestamp(), format.json()), - maxFiles: '14d', + maxFiles: process.env.LOG_RETENTION_PERIOD || '14d', level: LOG_LEVEL, dirname: LOG_DIR, datePattern: 'YYYY-MM-DD',