diff --git a/.env.example b/.env.example index 7ac4cac..e6e26e6 100644 --- a/.env.example +++ b/.env.example @@ -5,3 +5,4 @@ DATABASE_URL=postgres://user:pass@localhost:5432/dbname TEST_DATABASE_URL=postgres://user:pass@localhost:5432/test-dbname JWT_SECRET=changeIt! JWT_EXPIRATION=7d +SENTRY_DSN=https://@sentry.io/ diff --git a/.eslintrc b/.eslintrc index d171caa..c82b3be 100644 --- a/.eslintrc +++ b/.eslintrc @@ -4,6 +4,9 @@ "airbnb", "plugin:jsx-a11y/recommended" ], + "globals": { + "Kit": true + }, "env": { "node": true, "browser": true, diff --git a/README.md b/README.md index 7bd3d78..a90ddea 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,14 @@ This starter kit is designed to help you start project as soon as possible. It contain all necessary things to develop and maintain a project. +## Before start + +You need to read these docs: + +1. [Error handling](/docs/error.md) + +Remove all code which we use for showing template opportunities. We left indicator for every place which need to remove `// REMOVE_PROD` + ## Getting started In this project we used [ESLint](https://eslint.org/docs/user-guide/integrations#editors) and [Stylelint](https://stylelint.io/user-guide/complementary-tools/#editor-plugins), so at the beginning check your code editor plugins or settings to support these linters. @@ -112,3 +120,132 @@ Service worker file is generated and connected via [Workbox webpack plugin](http ## Swagger Project uses [Swagger](https://swagger.io) to document documentation API routes. You can check [example comments](https://github.com/keenethics/node-react-starter-kit/blob/60e07d395300961f3971f8586e2e23d2dbd0f5ea/server/routes/user.route.js#L9) and follow same [convention](https://swagger.io/docs/specification/basic-structure/). + +## Error handling + +This is a document how we most work with **Error** on current project. + +For correct works with **Error** in project please read [this article](https://expressjs.com/en/guide/error-handling.html) and use `Kit.CustomError` as `Error` object + +Example of handling: + +```js +router.get('/error', (req, res, next) => { + try { + // ...some code + } catch(e) { + next(new Kit.CustomError('UNAUTHORIZED_ACCESS', 401)); + } +}); +``` + +### Error structure + +This structure you must send to client when error is happened and you can get this structure from `Kit.CustomError` by `.get()` method + +```js +{ + "errors": [ + { + "parameter": "start_time", + "details": "invalid date", + "code": "INVALID_PARAMETER", + "value": "", + "message": "Expected time, got \"\" for start_time", + "userMessage": "Expected time, got \"\" for start_time" + } + ], + "request": { + "params": { + "account_id": "hkk5" + } + }, + "metadata": {} +} +``` + +## Error codes & what they mean + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
HTTP CodeError Code
403ACCOUNT_NOT_FOUND
403ACTION_NOT_ALLOWED
400EXCLUSIVE_PARAMETERS
400FEATURE_NOT_AVAILABLE
400ILLEGAL_CHARACTERS
500INTERNAL_ERROR
400INVALID_PARAMETER
400INVALID_USER
400INVALID_USER_ID
400MISSING_PARAMETERS
404NOT_FOUND
400REQUEST_TOO_COMPLEX
404ROUTE_NOT_FOUND
503SERVICE_UNAVAILABLE
503OVER_CAPACITY
429TOO_MANY_REQUESTS
401UNAUTHORIZED_ACCESS
403USER_NOT_FOUND
+ diff --git a/client/components/common/Navigation.jsx b/client/components/common/Navigation.jsx index 6019119..0dc6e5a 100644 --- a/client/components/common/Navigation.jsx +++ b/client/components/common/Navigation.jsx @@ -15,6 +15,9 @@ const Navigation = () => (
  • Form components
  • +
  • + Example page +
  • diff --git a/client/pages/Example.jsx b/client/pages/Example.jsx new file mode 100644 index 0000000..d96e9d6 --- /dev/null +++ b/client/pages/Example.jsx @@ -0,0 +1,30 @@ +import React, { Component } from 'react'; + +import { Button } from 'Form'; + +class ExampleComponents extends Component { + state = { + error: '', + } + + showError = () => { + fetch('/api/test/error') + .then(response => response.json()) + .then(data => this.setState({ error: JSON.stringify(data) })); + } + + render() { + const { error } = this.state; + + return ( +
    + +

    + {`Error: ${error}`} +

    +
    + ); + } +} + +export default ExampleComponents; diff --git a/client/routing/Routes.jsx b/client/routing/Routes.jsx index 06e34ba..85a6dfc 100644 --- a/client/routing/Routes.jsx +++ b/client/routing/Routes.jsx @@ -5,6 +5,7 @@ import MainLayout from 'Layout/Main'; import Index from 'Pages/Index'; import FormComponents from 'Pages/FormComponents'; +import ExampleComponents from 'Pages/Example'; import NoMatch from 'Pages/NoMatch'; const Routes = () => ( @@ -13,6 +14,7 @@ const Routes = () => ( + diff --git a/package-lock.json b/package-lock.json index 53004cb..9aa3148 100644 --- a/package-lock.json +++ b/package-lock.json @@ -877,6 +877,67 @@ "integrity": "sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw==", "dev": true }, + "@sentry/core": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-5.5.0.tgz", + "integrity": "sha512-xOcBud0t5mfhFdyd2tQQti4uuWSrLiJihpXzxeRpdCfk2ic+xmpeQs3G4UqnluvQDc48ug/Igt7LXfSBRBx4eg==", + "requires": { + "@sentry/hub": "5.5.0", + "@sentry/minimal": "5.5.0", + "@sentry/types": "5.5.0", + "@sentry/utils": "5.5.0", + "tslib": "^1.9.3" + } + }, + "@sentry/hub": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@sentry/hub/-/hub-5.5.0.tgz", + "integrity": "sha512-+jKh5U1nv8ufoquGciWoZPOmKuEjFPH5m0VifCs6t3NcEbAq2qnfF26KUGqhUNznlUN/PkbWB4qMfKn14uNE2Q==", + "requires": { + "@sentry/types": "5.5.0", + "@sentry/utils": "5.5.0", + "tslib": "^1.9.3" + } + }, + "@sentry/minimal": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@sentry/minimal/-/minimal-5.5.0.tgz", + "integrity": "sha512-o6O30+/pNrO7fTgwKxgZynHB7cMScJlw9HXgnNXgLXS6LBiqjYCQfVnWAgV//SyyG0uUlcjH3P6PnV6TsJOmVQ==", + "requires": { + "@sentry/hub": "5.5.0", + "@sentry/types": "5.5.0", + "tslib": "^1.9.3" + } + }, + "@sentry/node": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@sentry/node/-/node-5.5.0.tgz", + "integrity": "sha512-Qn60k7NqJhzpI7PnW/dOz2Y1ofD6kMKGEgLWAO5vcbNShyQj7m2JYFk0c7nFU9HDgertqkxQnvhvIGvT+QokaQ==", + "requires": { + "@sentry/core": "5.5.0", + "@sentry/hub": "5.5.0", + "@sentry/types": "5.5.0", + "@sentry/utils": "5.5.0", + "cookie": "0.3.1", + "https-proxy-agent": "2.2.1", + "lru_map": "0.3.3", + "tslib": "^1.9.3" + } + }, + "@sentry/types": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@sentry/types/-/types-5.5.0.tgz", + "integrity": "sha512-3otF/miVDth91o+iign00x0o31McUPeyIFbMjLbgeTRRW9rXpu2jGrcRrvHfofECtoqCf5Y734hwvvlBvFZeIw==" + }, + "@sentry/utils": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-5.5.0.tgz", + "integrity": "sha512-gO8Bs/QcKDn7ncc2f2fIOTPx2AiZKrGj4us1Yxu6mBU8JZqMQRl9XjDMFAUECJQvquBAta+TFJyYj71ZedeQUQ==", + "requires": { + "@sentry/types": "5.5.0", + "tslib": "^1.9.3" + } + }, "@types/geojson": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-1.0.6.tgz", @@ -1151,7 +1212,6 @@ "version": "4.2.1", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.2.1.tgz", "integrity": "sha512-JVwXMr9nHYTUXsBFKUqhJwvlcYU/blreOEUkhNR2eXZIvwd+c+o5V4MgDPKWnMS/56awN3TRzIP+KoPn+roQtg==", - "dev": true, "requires": { "es6-promisify": "^5.0.0" } @@ -1584,7 +1644,7 @@ }, "array-equal": { "version": "1.0.0", - "resolved": "http://registry.npmjs.org/array-equal/-/array-equal-1.0.0.tgz", + "resolved": "https://registry.npmjs.org/array-equal/-/array-equal-1.0.0.tgz", "integrity": "sha1-jCpe8kcv2ep0KwTHenUJO6J1fJM=", "dev": true }, @@ -1602,7 +1662,7 @@ }, "array-flatten": { "version": "1.1.1", - "resolved": "http://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" }, "array-includes": { @@ -1679,7 +1739,7 @@ }, "util": { "version": "0.10.3", - "resolved": "http://registry.npmjs.org/util/-/util-0.10.3.tgz", + "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz", "integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=", "dev": true, "requires": { @@ -2010,7 +2070,7 @@ }, "babel-plugin-syntax-object-rest-spread": { "version": "6.13.0", - "resolved": "http://registry.npmjs.org/babel-plugin-syntax-object-rest-spread/-/babel-plugin-syntax-object-rest-spread-6.13.0.tgz", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-object-rest-spread/-/babel-plugin-syntax-object-rest-spread-6.13.0.tgz", "integrity": "sha1-/WU28rzhODb/o6VFjEkDpZe7O/U=", "dev": true }, @@ -2430,7 +2490,7 @@ "dependencies": { "resolve": { "version": "1.1.7", - "resolved": "http://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz", "integrity": "sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=", "dev": true } @@ -2444,7 +2504,7 @@ }, "browserify-aes": { "version": "1.2.0", - "resolved": "http://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", + "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==", "dev": true, "requires": { @@ -2481,7 +2541,7 @@ }, "browserify-rsa": { "version": "4.0.1", - "resolved": "http://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz", + "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz", "integrity": "sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=", "dev": true, "requires": { @@ -2535,7 +2595,7 @@ }, "buffer": { "version": "4.9.1", - "resolved": "http://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz", "integrity": "sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg=", "dev": true, "requires": { @@ -2689,7 +2749,7 @@ }, "camelcase-keys": { "version": "2.1.0", - "resolved": "http://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz", + "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz", "integrity": "sha1-MIvur/3ygRkFHvodkyITyRuPkuc=", "dev": true, "requires": { @@ -3100,7 +3160,7 @@ }, "clone-deep": { "version": "0.2.4", - "resolved": "http://registry.npmjs.org/clone-deep/-/clone-deep-0.2.4.tgz", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-0.2.4.tgz", "integrity": "sha1-TnPdCen7lxzDhnDF3O2cGJZIHMY=", "dev": true, "requires": { @@ -3463,7 +3523,7 @@ }, "create-hash": { "version": "1.2.0", - "resolved": "http://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", + "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", "dev": true, "requires": { @@ -3476,7 +3536,7 @@ }, "create-hmac": { "version": "1.1.7", - "resolved": "http://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", + "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", "dev": true, "requires": { @@ -3527,7 +3587,7 @@ }, "css-color-names": { "version": "0.0.4", - "resolved": "http://registry.npmjs.org/css-color-names/-/css-color-names-0.0.4.tgz", + "resolved": "https://registry.npmjs.org/css-color-names/-/css-color-names-0.0.4.tgz", "integrity": "sha1-gIrcLnnPhHOAabZGyyDsJ762KeA=", "dev": true }, @@ -4109,7 +4169,7 @@ }, "diffie-hellman": { "version": "5.0.3", - "resolved": "http://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", + "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", "integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==", "dev": true, "requires": { @@ -4470,14 +4530,12 @@ "es6-promise": { "version": "4.2.5", "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.5.tgz", - "integrity": "sha512-n6wvpdE43VFtJq+lUDYDBFUwV8TZbuGXLV4D6wKafg13ldznKsyEvatubnmUe31zcvelSzOHF+XbaT+Bl9ObDg==", - "dev": true + "integrity": "sha512-n6wvpdE43VFtJq+lUDYDBFUwV8TZbuGXLV4D6wKafg13ldznKsyEvatubnmUe31zcvelSzOHF+XbaT+Bl9ObDg==" }, "es6-promisify": { "version": "5.0.0", - "resolved": "http://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz", + "resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz", "integrity": "sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM=", - "dev": true, "requires": { "es6-promise": "^4.0.3" } @@ -4722,7 +4780,7 @@ "dependencies": { "doctrine": { "version": "1.5.0", - "resolved": "http://registry.npmjs.org/doctrine/-/doctrine-1.5.0.tgz", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-1.5.0.tgz", "integrity": "sha1-N53Ocw9hZvds76TmcHoVmwLFpvo=", "dev": true, "requires": { @@ -6194,12 +6252,14 @@ "balanced-match": { "version": "1.0.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, "dev": true, + "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -6219,7 +6279,8 @@ "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "console-control-strings": { "version": "1.1.0", @@ -6367,6 +6428,7 @@ "version": "3.0.4", "bundled": true, "dev": true, + "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -6750,7 +6812,7 @@ }, "strip-ansi": { "version": "3.0.1", - "resolved": "http://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", "dev": true, "requires": { @@ -6865,7 +6927,7 @@ }, "global-modules": { "version": "0.2.3", - "resolved": "http://registry.npmjs.org/global-modules/-/global-modules-0.2.3.tgz", + "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-0.2.3.tgz", "integrity": "sha1-6lo77ULG1s6ZWk+KEmm12uIjgo0=", "dev": true, "requires": { @@ -6889,7 +6951,7 @@ }, "global-prefix": { "version": "0.1.5", - "resolved": "http://registry.npmjs.org/global-prefix/-/global-prefix-0.1.5.tgz", + "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-0.1.5.tgz", "integrity": "sha1-jTvGuNo8qBEqFg2NSW/wRiv+948=", "dev": true, "requires": { @@ -6970,7 +7032,7 @@ "dependencies": { "minimist": { "version": "1.1.3", - "resolved": "http://registry.npmjs.org/minimist/-/minimist-1.1.3.tgz", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.1.3.tgz", "integrity": "sha1-O+39kaktOQFvz6ocaB6Pqhoe/ag=", "dev": true } @@ -7409,7 +7471,7 @@ }, "html-webpack-plugin": { "version": "3.2.0", - "resolved": "http://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-3.2.0.tgz", + "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-3.2.0.tgz", "integrity": "sha1-sBq71yOsqqeze2r0SS69oD2d03s=", "dev": true, "requires": { @@ -7424,7 +7486,7 @@ "dependencies": { "json5": { "version": "0.5.1", - "resolved": "http://registry.npmjs.org/json5/-/json5-0.5.1.tgz", + "resolved": "https://registry.npmjs.org/json5/-/json5-0.5.1.tgz", "integrity": "sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE=", "dev": true }, @@ -7497,7 +7559,7 @@ }, "http-errors": { "version": "1.6.3", - "resolved": "http://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=", "requires": { "depd": "~1.1.2", @@ -7847,7 +7909,6 @@ "version": "2.2.1", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-2.2.1.tgz", "integrity": "sha512-HPCTS1LW51bcyMYbxUIOO4HEOlQ1/1qRaFWcyxvwaqUS9TY88aoEuHUY33kuAh1YhVVaDQhLZsnPd+XNARWZlQ==", - "dev": true, "requires": { "agent-base": "^4.1.0", "debug": "^3.1.0" @@ -7857,7 +7918,6 @@ "version": "3.2.6", "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", - "dev": true, "requires": { "ms": "^2.1.1" } @@ -7865,8 +7925,7 @@ "ms": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", - "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", - "dev": true + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" } } }, @@ -8394,7 +8453,7 @@ }, "is-obj": { "version": "1.0.1", - "resolved": "http://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", "integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=", "dev": true }, @@ -9897,7 +9956,7 @@ }, "load-json-file": { "version": "2.0.0", - "resolved": "http://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz", "integrity": "sha1-eUfkIUmvgNaWy/eXvKq8/h/inKg=", "dev": true, "requires": { @@ -9918,7 +9977,7 @@ }, "pify": { "version": "2.3.0", - "resolved": "http://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", "dev": true } @@ -10154,6 +10213,11 @@ "es5-ext": "~0.10.2" } }, + "lru_map": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/lru_map/-/lru_map-0.3.3.tgz", + "integrity": "sha1-tcg1G5Rky9dQM1p5ZQoOwOVhGN0=" + }, "make-dir": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz", @@ -10253,7 +10317,7 @@ }, "media-typer": { "version": "0.3.0", - "resolved": "http://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" }, "mem": { @@ -10289,7 +10353,7 @@ }, "meow": { "version": "3.7.0", - "resolved": "http://registry.npmjs.org/meow/-/meow-3.7.0.tgz", + "resolved": "https://registry.npmjs.org/meow/-/meow-3.7.0.tgz", "integrity": "sha1-cstmi0JSKCkKu/qFaJJYcwioAfs=", "dev": true, "requires": { @@ -10317,7 +10381,7 @@ }, "load-json-file": { "version": "1.1.0", - "resolved": "http://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", "dev": true, "requires": { @@ -10359,7 +10423,7 @@ }, "pify": { "version": "2.3.0", - "resolved": "http://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", "dev": true }, @@ -10618,7 +10682,7 @@ }, "mkdirp": { "version": "0.5.1", - "resolved": "http://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", "requires": { "minimist": "0.0.8" @@ -10882,7 +10946,7 @@ "dependencies": { "semver": { "version": "5.3.0", - "resolved": "http://registry.npmjs.org/semver/-/semver-5.3.0.tgz", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.3.0.tgz", "integrity": "sha1-myzl094C0XxgEq0yaqa00M9U+U8=", "dev": true } @@ -10995,7 +11059,7 @@ }, "chalk": { "version": "1.1.3", - "resolved": "http://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", "dev": true, "requires": { @@ -11024,7 +11088,7 @@ }, "strip-ansi": { "version": "3.0.1", - "resolved": "http://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", "dev": true, "requires": { @@ -11429,7 +11493,7 @@ }, "os-homedir": { "version": "1.0.2", - "resolved": "http://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=" }, "os-locale": { @@ -11444,7 +11508,7 @@ }, "os-tmpdir": { "version": "1.0.2", - "resolved": "http://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=" }, "osenv": { @@ -11631,7 +11695,7 @@ }, "path-is-absolute": { "version": "1.0.1", - "resolved": "http://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" }, "path-is-inside": { @@ -11666,7 +11730,7 @@ "dependencies": { "pify": { "version": "2.3.0", - "resolved": "http://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", "dev": true } @@ -12987,7 +13051,7 @@ }, "readable-stream": { "version": "2.3.6", - "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", "dev": true, "requires": { @@ -13008,7 +13072,7 @@ }, "string_decoder": { "version": "1.1.1", - "resolved": "http://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "dev": true, "requires": { @@ -13432,7 +13496,7 @@ "dependencies": { "jsesc": { "version": "0.5.0", - "resolved": "http://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", "integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=", "dev": true } @@ -13527,7 +13591,7 @@ }, "css-select": { "version": "1.2.0", - "resolved": "http://registry.npmjs.org/css-select/-/css-select-1.2.0.tgz", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-1.2.0.tgz", "integrity": "sha1-KzoRBTnFNV8c2NMUYj6HCxIeyFg=", "dev": true, "requires": { @@ -13549,7 +13613,7 @@ }, "strip-ansi": { "version": "3.0.1", - "resolved": "http://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", "dev": true, "requires": { @@ -13725,7 +13789,7 @@ }, "resolve-dir": { "version": "0.1.1", - "resolved": "http://registry.npmjs.org/resolve-dir/-/resolve-dir-0.1.1.tgz", + "resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-0.1.1.tgz", "integrity": "sha1-shklmlYC+sXFxJatiUpujMQwJh4=", "dev": true, "requires": { @@ -13851,7 +13915,7 @@ }, "safe-regex": { "version": "1.1.0", - "resolved": "http://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", + "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", "dev": true, "requires": { @@ -14227,7 +14291,7 @@ }, "load-json-file": { "version": "1.1.0", - "resolved": "http://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", "dev": true, "requires": { @@ -14240,7 +14304,7 @@ }, "os-locale": { "version": "1.4.0", - "resolved": "http://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz", + "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz", "integrity": "sha1-IPnxeuKe00XoveWDsT0gCYA8FNk=", "dev": true, "requires": { @@ -14278,7 +14342,7 @@ }, "pify": { "version": "2.3.0", - "resolved": "http://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", "dev": true }, @@ -14305,7 +14369,7 @@ }, "string-width": { "version": "1.0.2", - "resolved": "http://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", "dev": true, "requires": { @@ -14316,7 +14380,7 @@ }, "strip-ansi": { "version": "3.0.1", - "resolved": "http://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", "dev": true, "requires": { @@ -14686,7 +14750,7 @@ }, "sha.js": { "version": "2.4.11", - "resolved": "http://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", "dev": true, "requires": { @@ -14708,7 +14772,7 @@ "dependencies": { "kind-of": { "version": "2.0.1", - "resolved": "http://registry.npmjs.org/kind-of/-/kind-of-2.0.1.tgz", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-2.0.1.tgz", "integrity": "sha1-AY7HpM5+OobLkUG+UZ0kyPqpgbU=", "dev": true, "requires": { @@ -15341,7 +15405,7 @@ }, "strip-eof": { "version": "1.0.0", - "resolved": "http://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", + "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=" }, "strip-indent": { @@ -16276,7 +16340,7 @@ }, "tapable": { "version": "0.1.10", - "resolved": "http://registry.npmjs.org/tapable/-/tapable-0.1.10.tgz", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-0.1.10.tgz", "integrity": "sha1-KcNXB8K3DlDQdIK10gLo7URtr9Q=", "dev": true }, @@ -16541,7 +16605,7 @@ }, "through": { "version": "2.3.8", - "resolved": "http://registry.npmjs.org/through/-/through-2.3.8.tgz", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=" }, "through2": { @@ -17368,7 +17432,7 @@ }, "chalk": { "version": "1.1.3", - "resolved": "http://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", "dev": true, "requires": { @@ -17381,7 +17445,7 @@ }, "strip-ansi": { "version": "3.0.1", - "resolved": "http://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", "dev": true, "requires": { @@ -18048,7 +18112,7 @@ }, "strip-ansi": { "version": "3.0.1", - "resolved": "http://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", "dev": true, "requires": { @@ -18387,7 +18451,7 @@ }, "wrap-ansi": { "version": "2.1.0", - "resolved": "http://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", "requires": { "string-width": "^1.0.1", @@ -18419,7 +18483,7 @@ }, "strip-ansi": { "version": "3.0.1", - "resolved": "http://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", "requires": { "ansi-regex": "^2.0.0" diff --git a/package.json b/package.json index 423a089..281c265 100644 --- a/package.json +++ b/package.json @@ -65,6 +65,7 @@ "workbox-webpack-plugin": "^3.6.3" }, "dependencies": { + "@sentry/node": "^5.5.0", "bcryptjs": "^2.4.3", "body-parser": "^1.18.3", "classcat": "^3.2.3", diff --git a/server/config/error.js b/server/config/error.js new file mode 100644 index 0000000..0f40a3e --- /dev/null +++ b/server/config/error.js @@ -0,0 +1,97 @@ +module.exports = { + ACCOUNT_NOT_FOUND: { + message: 'Account not found', + userMessage: 'Account not found', + code: 403, + }, + ACTION_NOT_ALLOWED: { + message: 'Action not allowed', + userMessage: 'Action not allowed', + code: 403, + }, + EXCLUSIVE_PARAMETERS: { + message: 'Exclusive parameters', + userMessage: 'Exclusive parameters', + code: 400, + }, + FEATURE_NOT_AVAILABLE: { + message: 'Feature not available', + userMessage: 'Feature not available', + code: 400, + }, + ILLEGAL_CHARACTERS: { + message: 'Illegal characters', + userMessage: 'Illegal characters', + code: 400, + }, + INTERNAL_ERROR: { + message: 'Server error', + userMessage: 'Server error', + code: 500, + }, + INVALID_PARAMETER: { + message: 'Invalid parameter', + userMessage: 'Invalid parameter', + code: 400, + }, + INVALID_USER: { + message: 'Invalid user', + userMessage: 'Invalid user', + code: 400, + }, + INVALID_USER_ID: { + message: 'Invalid user ID', + userMessage: 'Invalid user ID', + code: 400, + }, + MISSING_PARAMETERS: { + message: 'Missing parameters', + userMessage: 'Missing parameters', + code: 400, + }, + NOT_FOUND: { + message: 'Not Found', + userMessage: 'Not Found', + code: 404, + }, + REQUEST_TOO_COMPLEX: { + message: 'Request too complex', + userMessage: 'Request too complex', + code: 400, + }, + ROUTE_NOT_FOUND: { + message: 'Route not found', + userMessage: 'Route not found', + code: 404, + }, + SERVICE_UNAVAILABLE: { + message: 'Service unavailable', + userMessage: 'Service unavailable', + code: 503, + }, + OVER_CAPACITY: { + message: 'Over capacity', + userMessage: 'Over capacity', + code: 503, + }, + TOO_MANY_REQUESTS: { + message: 'Too many request', + userMessage: 'Too many request', + code: 429, + }, + UNAUTHORIZED_ACCESS: { + message: 'Unathorized access', + userMessage: 'Unathorized access', + code: 401, + }, + USER_NOT_FOUND: { + message: 'User not found', + userMessage: 'User not found', + code: 403, + }, + FORBIDDEN: { + message: 'Forbidden', + userMessage: 'Forbidden', + code: 403, + }, +}; diff --git a/server/index.js b/server/index.js index 2752856..3d9bf9b 100644 --- a/server/index.js +++ b/server/index.js @@ -6,12 +6,20 @@ const cors = require('cors'); const helmet = require('helmet'); const compression = require('compression'); const methodOverride = require('method-override'); +const Sentry = require('@sentry/node'); const Swagger = require('./swagger'); const routes = require('./routes/index.route'); +const CustomError = require('./utils/error'); +const errorHandler = require('./middlewares/error'); require('dotenv-safe').config(); +global.Kit = {}; +Kit.CustomError = CustomError; + +Sentry.init({ dsn: process.env.SENTRY_DSN }); + const app = express(); app.db = require('./models'); @@ -30,9 +38,16 @@ app.use('/api', routes); if (!isProduction) { Swagger(app); } +// REMOVE_PROD: in real app you need remove this route +app.get('/debug-sentry', () => { + throw new Kit.CustomError(); +}); app.get('/*', (req, res) => res.sendFile(path.join(__dirname, `../${isProduction ? 'dist' : 'client'}/index.html`))); +app.use(Sentry.Handlers.errorHandler()); +app.use(errorHandler); + const port = process.env.APP_PORT || 3001; const host = process.env.APP_HOST || 'localhost'; diff --git a/server/middlewares/error.js b/server/middlewares/error.js new file mode 100644 index 0000000..9a943fc --- /dev/null +++ b/server/middlewares/error.js @@ -0,0 +1,19 @@ +function errorHandling(err, req, res, next) { + if (res.headersSent) { + return next(err); + } + + res.status(err.httpCode); + return res.json(err.get()); +} + +// process.on('unhandledRejection', (err) => { +// console.error(err); +// }); + +// process.on('uncaughtException', (err) => { +// console.error(err); +// process.exit(1); +// }); + +module.exports = errorHandling; diff --git a/server/routes/index.route.js b/server/routes/index.route.js index ff85702..3456095 100644 --- a/server/routes/index.route.js +++ b/server/routes/index.route.js @@ -2,6 +2,8 @@ const express = require('express'); const authRoutes = require('./auth.route'); const userRoutes = require('./user.route'); +// REMOVE_PROD: in real app you need remove this variable +const testRoutes = require('./test.route'); const router = express.Router(); @@ -11,4 +13,8 @@ router.use('/auth', authRoutes); // mount user routes at /user router.use('/user', userRoutes); +// REMOVE_PROD: in real app you need remove this route +router.use('/test', testRoutes); + + module.exports = router; diff --git a/server/routes/test.route.js b/server/routes/test.route.js new file mode 100644 index 0000000..f07b80f --- /dev/null +++ b/server/routes/test.route.js @@ -0,0 +1,9 @@ +const express = require('express'); + +const router = express.Router(); + +router.get('/error', (req, res, next) => { + next(new Kit.CustomError('UNAUTHORIZED_ACCESS', 401)); +}); + +module.exports = router; diff --git a/server/tests/error.test.js b/server/tests/error.test.js new file mode 100644 index 0000000..a42ee49 --- /dev/null +++ b/server/tests/error.test.js @@ -0,0 +1,35 @@ +// REMOVE_PROD: remove this file + +const _ = require('lodash'); +const base = require('./base.test'); +const Errors = require('../config/error'); + +const { request, truncate } = base; +const apiBase = process.env.API_BASE || '/api'; + +describe('GET /test/error', () => { + before(() => truncate()); + after(() => truncate()); + + it('error structure', () => request + .get(`${apiBase}/test/error`) + .expect('Content-Type', /json/) + .expect(401) + .expect(({ body }) => { + if (!(Array.isArray(body.errors) && body.errors.length && body.request && body.metadata)) { + throw new Error(` + Error response must have next structure: + { + "errors": [{ ... }], + "request": { ... }, + "metadata": {} + } + `); + } + const error = _.get(body, 'errors[0]', {}); + + if (error.userMessage !== Errors.UNAUTHORIZED_ACCESS.userMessage && error.code !== 401) { + throw new Error('Error has incorrect data'); + } + })); +}); diff --git a/server/utils/error.js b/server/utils/error.js new file mode 100644 index 0000000..d7920ed --- /dev/null +++ b/server/utils/error.js @@ -0,0 +1,69 @@ +const _ = require('lodash'); +const ErrorHash = require('../config/error'); + +const isProduction = process.env.NODE_ENV === 'production'; + +const defaultError = { + parameter: '', + details: '', + code: 'ERROR', + value: '', + message: 'Something went wrong.', + userMessage: 'Something went wrong.', + httpCode: 500, +}; + +class CustomError extends Error { + static createError(errorCode) { + const error = ErrorHash[errorCode]; + + if (!error) { + return { + ...defaultError, + code: errorCode, + }; + } + + return { + ...defaultError, + ...error, + }; + } + + constructor(errorCode, httpCode, ...params) { + super(params); + this.errors = [CustomError.createError(errorCode)]; + this.code = httpCode || this.errors[0].httpCode; + this.message = this.errors[0].message; + this.name = 'Server error'; + this.request = {}; + this.metadata = {}; + } + + get() { + const errors = this.errors.slice().map((error) => { + if (isProduction) { + // eslint-disable-next-line + delete error.message; + } + + return error; + }); + + return { + errors, + request: this.request, + metadata: this.metadata, + }; + } + + upsertMetadata(data) { + if (!_.isObject(data)) { + throw new Error("Metadata must be 'object'"); + } + + this.metadata = this.metadata ? { ...this.metadata, ...data } : data; + } +} + +module.exports = CustomError;