[
  {
    "path": ".commitlintrc.ts",
    "content": "import type { UserConfig } from '@commitlint/types';\n\nconst commitlintConfig: UserConfig = {\n  extends: ['@commitlint/config-conventional'],\n};\n\nmodule.exports = commitlintConfig;\n"
  },
  {
    "path": ".czrc",
    "content": "{\n  \"path\": \"node_modules/cz-conventional-changelog\"\n}\n"
  },
  {
    "path": ".dockerignore",
    "content": "config\ndatabase\ndist\nnode_modules\nscripts\n.git\n.env"
  },
  {
    "path": ".eslintrc.js",
    "content": "module.exports = {\n  parser: '@typescript-eslint/parser',\n\n  extends: [\n    'plugin:@typescript-eslint/recommended',\n    'prettier',\n    'plugin:prettier/recommended',\n    'plugin:no-array-reduce/recommended',\n  ],\n\n  plugins: ['@typescript-eslint', 'import'],\n\n  rules: {\n    // General\n    '@typescript-eslint/no-unused-vars': 0,\n    '@typescript-eslint/explicit-module-boundary-types': 0,\n    '@typescript-eslint/no-explicit-any': 0,\n    '@typescript-eslint/no-non-null-assertion': 0,\n    '@typescript-eslint/ban-ts-comment': 0,\n    '@typescript-eslint/no-empty-interface': 0,\n\n    // Import\n    'import/order': [\n      'error',\n      {\n        groups: ['builtin', 'external', 'internal', 'parent', 'sibling'],\n        'newlines-between': 'always',\n        alphabetize: {\n          order: 'asc',\n          caseInsensitive: true,\n        },\n      },\n    ],\n  },\n\n  parserOptions: {\n    ecmaVersion: 2018,\n    sourceType: 'module',\n  },\n\n  settings: {\n    'import/resolver': {\n      node: {\n        extensions: ['.js', '.jsx', '.ts', '.tsx'],\n        moduleDirectory: ['node_modules', 'src/'],\n      },\n    },\n  },\n};\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.yml",
    "content": "name: 'Bug Report'\ndescription: 'File a bug report'\nbody:\n  - type: 'textarea'\n    id: 'description'\n    attributes:\n      label: 'Description'\n      description: 'A clear and concise description of what the bug is.'\n      placeholder: |\n        Bug description\n    validations:\n      required: true\n  - type: 'textarea'\n    id: 'additional-information'\n    attributes:\n      label: 'Additional Information'\n      description: |\n        Add any other context about the problem here.\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "content": "blank_issues_enabled: false\ncontact_links:\n  - name: Ask a question\n    url: https://github.com/mkosir/express-typescript-typeorm-boilerplate/discussions\n    about: Ask questions and discuss topics with other community members\n"
  },
  {
    "path": ".github/workflows/main.yml",
    "content": "name: CI\n\non: push\n\njobs:\n  validate:\n    runs-on: ubuntu-latest\n\n    steps:\n      - name: Checkout 🛎️\n        uses: actions/checkout@v2\n      - uses: actions/setup-node@v2\n        with:\n          node-version: '16.x'\n\n      - name: Cache dependencies ⚡\n        id: cache\n        uses: actions/cache@v2\n        with:\n          path: node_modules\n          key: node-modules-${{ hashFiles('package-lock.json') }}\n\n      - name: Install dependencies 🔧\n        if: steps.cache.outputs.cache-hit != 'true'\n        run: npm ci\n\n      - name: Lint ✅\n        run: npm run lint\n\n      - name: Build 🏗️\n        run: npm run build\n"
  },
  {
    "path": ".gitignore",
    "content": ".idea/\n.vscode/\n.DS_Store\n\nnode_modules/\nbuild/\ndist/\ntmp/\ntemp/\n\ndatabase/*\n!database/.gitkeep \n\nlog/*\n!log/.gitkeep \n*.log\n\n# keep config folder with env files ONLY for demo purposes\n# config/\n# .env"
  },
  {
    "path": ".husky/commit-msg",
    "content": "#!/bin/sh\n. \"$(dirname \"$0\")/_/husky.sh\"\n\nnpx --no-install commitlint --edit \"$1\""
  },
  {
    "path": ".husky/pre-commit",
    "content": "#!/bin/sh\n. \"$(dirname \"$0\")/_/husky.sh\"\n\nnpm run lint-staged-husky"
  },
  {
    "path": ".husky/pre-push",
    "content": "#!/bin/sh\n. \"$(dirname \"$0\")/_/husky.sh\"\n\nnpm run build\n"
  },
  {
    "path": ".lintstagedrc",
    "content": "{\n  \"*\": [\n    \"pretty-quick --staged\"\n  ],\n  \"*.{js,jsx,ts,tsx}\": [\n    \"eslint --max-warnings 0\",\n    \"tsc-files --noEmit\"\n  ]\n}\n"
  },
  {
    "path": ".prettierignore",
    "content": "dist"
  },
  {
    "path": ".prettierrc",
    "content": "{\n  \"printWidth\": 120,\n  \"semi\": true,\n  \"singleQuote\": true,\n  \"trailingComma\": \"all\"\n}\n"
  },
  {
    "path": "Dockerfile",
    "content": "FROM node:16.14.0-alpine\n\nWORKDIR /app\n\nCOPY ./package.json .\nCOPY ./package-lock.json .\n\nRUN npm install && npm cache clean --force\n\nCOPY . .\n\nRUN npm run build\n\nEXPOSE 4000\n\nCMD [ \"npm\", \"start\" ]"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2020 mkosir\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "#  TypeORM / Express / TypeScript RESTful API boilerplate\n\n[![CI][build-badge]][build-url]\n[![TypeScript][typescript-badge]][typescript-url]\n[![prettier][prettier-badge]][prettier-url]\n![Heisenberg](misc/heisenberg.png)\n\nBoilerplate with focus on best practices and painless developer experience:\n\n- Minimal setup that can be extended 🔧\n- Spin it up with single command 🌀\n- TypeScript first\n- RESTful APIs\n- JWT authentication with role based authorization\n\n## Requirements\n\n- [Node v16+](https://nodejs.org/)\n- [Docker](https://www.docker.com/)\n\n## Running\n\n_Easily set up a local development environment with single command!_\n\n- clone the repo\n- `npm run docker:dev` 🚀\n\nVisit [localhost:4000](http://localhost:4000/) or if using Postman grab [config](/postman).\n\n### _What happened_ 💥\n\nContainers created:\n\n- Postgres database container seeded with 💊 Breaking Bad characters in `Users` table (default credentials `user=walter`, `password=white` in [.env file](./.env))\n- Node (v16 Alpine) container with running boilerplate RESTful API service\n- and one Node container instance to run tests locally or in CI\n\n## Features:\n\n- [Express](https://github.com/expressjs/express) framework\n- [TypeScript v4](https://github.com/microsoft/TypeScript) codebase\n- [TypeORM](https://typeorm.io/) using Data Mapper pattern\n- [Docker](https://www.docker.com/) environment:\n  - Easily start local development using [Docker Compose](https://docs.docker.com/compose/) with single command `npm run docker:dev`\n  - Connect to different staging or production environments `npm run docker:[stage|prod]`\n  - Ready for **microservices** development and deployment.  \n    Once API changes are made, just build and push new docker image with your favourite CI/CD tool  \n    `docker build -t <username>/api-boilerplate:latest .`  \n    `docker push <username>/api-boilerplate:latest`\n  - Run unit, integration (or setup with your frontend E2E) tests as `docker exec -ti be_boilerplate_test sh` and `npm run test`\n- Contract first REST API design:\n  - never break API again with HTTP responses and requests payloads using [type definitions](./src/types/express/index.d.ts)\n  - Consistent schema error [response](./src/utils/response/custom-error/types.ts). Your frontend will always know how to handle errors thrown in `try...catch` statements 💪\n- JWT authentication and role based authorization using custom middleware\n- Set local, stage or production [environmental variables](./config) with [type definitions](./src/types/ProcessEnv.d.ts)\n- Logging with [morgan](https://github.com/expressjs/morgan)\n- Unit and integration tests with [Mocha](https://mochajs.org/) and [Chai](https://www.chaijs.com/)\n- Linting with [ESLint](https://eslint.org/)\n- [Prettier](https://prettier.io/) code formatter\n- Git hooks with [Husky](https://github.com/typicode/husky) and [lint-staged](https://github.com/okonet/lint-staged)\n- Automated npm & Docker dependency updates with [Renovate](https://github.com/renovatebot/renovate) (set to patch version only)\n- Commit messages must meet [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/) format.  \n  After staging changes just run `npm run commit` and get instant feedback on your commit message formatting and be prompted for required fields by [Commitizen](https://github.com/commitizen/cz-cli)\n\n## Other awesome boilerplates:\n\nEach boilerplate comes with it's own flavor of libraries and setup, check out others:\n\n- [Express and TypeORM with TypeScript](https://github.com/typeorm/typescript-express-example)\n- [Node.js, Express.js & TypeScript Boilerplate for Web Apps](https://github.com/jverhoelen/node-express-typescript-boilerplate)\n- [Express boilerplate for building RESTful APIs](https://github.com/danielfsousa/express-rest-es2017-boilerplate)\n- [A delightful way to building a RESTful API with NodeJs & TypeScript by @w3tecch](https://github.com/w3tecch/express-typescript-boilerplate)\n\n[build-badge]: https://github.com/mkosir/express-typescript-typeorm-boilerplate/actions/workflows/main.yml/badge.svg\n[build-url]: https://github.com/mkosir/express-typescript-typeorm-boilerplate/actions/workflows/main.yml\n[typescript-badge]: https://badges.frapsoft.com/typescript/code/typescript.svg?v=101\n[typescript-url]: https://github.com/microsoft/TypeScript\n[prettier-badge]: https://img.shields.io/badge/code_style-prettier-ff69b4.svg\n[prettier-url]: https://github.com/prettier/prettier\n\n## Contributing\n\nAll contributions are welcome!\n"
  },
  {
    "path": "config/prod.env",
    "content": "NODE_ENV=production\n\nPORT=4000\n\n### Database - Postgres\nPG_HOST=host.production.com\nPG_PORT=5432\nPOSTGRES_USER=walter\nPOSTGRES_PASSWORD=white\nPOSTGRES_DB=production_boilerplate_db\n\n### JWT\nJWT_SECRET=4353dhf8gset8h523sfreh6qedn37dfposefsawd56n381jsd\nJWT_EXPIRATION=15m"
  },
  {
    "path": "config/stage.env",
    "content": "NODE_ENV=stage\n\nPORT=4000\n\n### Database - Postgres\nPG_HOST=host.stage.com\nPG_PORT=5432\nPOSTGRES_USER=walter\nPOSTGRES_PASSWORD=white\nPOSTGRES_DB=stage_boilerplate_db\n\n### JWT\nJWT_SECRET=ert423dhf8gh523reh6qedn37dfposdgawdn381js25w\nJWT_EXPIRATION=15m"
  },
  {
    "path": "database/.gitkeep",
    "content": ""
  },
  {
    "path": "docker-compose.dev.yml",
    "content": "version: '3'\n\nservices:\n  db_postgres:\n    container_name: 'db_boilerplate'\n    image: 'postgres:14.2-alpine'\n    restart: always\n    env_file:\n      - .env\n    ports:\n      - '5432:5432'\n    volumes:\n      - ./database/boilerplate:/var/lib/postgresql/data/\n\n  be_boilerplate:\n    entrypoint: /bin/sh './scripts/be-node-dev.sh'\n    env_file:\n      - .env\n    ports:\n      - '4000:4000'\n    depends_on:\n      - db_postgres\n    links:\n      - db_postgres\n\n  be_boilerplate_test:\n    container_name: 'be_boilerplate_test'\n    command: sh -c \"echo 'Test container ready' && tail -f /dev/null\"\n    build: .\n    stdin_open: true\n    tty: true\n    depends_on:\n      - db_postgres\n    links:\n      - db_postgres\n    env_file:\n      - .env\n    volumes:\n      - .:/app/\n      - /app/node_modules\n"
  },
  {
    "path": "docker-compose.prod.yml",
    "content": "version: '3'\n\nservices:\n  be_boilerplate:\n    deploy:\n      resources:\n        limits:\n          cpus: '0.90'\n          # memory: 400M\n    command: /bin/sh -c \"echo 'Running API on production!' && npm run build && npm start\"\n    ports:\n      - '4000:4000'\n    env_file:\n      - ./config/prod.env\n"
  },
  {
    "path": "docker-compose.stage.yml",
    "content": "version: '3'\n\nservices:\n  be_boilerplate:\n    command: /bin/sh -c \"echo 'Running API on production!' && npm run build && npm start\"\n    ports:\n      - '4000:4000'\n    env_file:\n      - ./config/stage.env\n"
  },
  {
    "path": "docker-compose.yml",
    "content": "version: '3'\n\nservices:\n  be_boilerplate:\n    container_name: 'be_boilerplate'\n    build: .\n    restart: always\n    volumes:\n      - .:/app/\n      - /app/node_modules\n"
  },
  {
    "path": "log/.gitkeep",
    "content": ""
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"express-typescript-typeorm-boilerplate\",\n  \"version\": \"1.0.0\",\n  \"description\": \"RESTful API boilerplate with Typescript, TypeORM, ExpressJs & Mocha.\",\n  \"scripts\": {\n    \"dev\": \"debug=* NODE_PATH=./src ts-node-dev --respawn ./src/index.ts\",\n    \"build\": \"rimraf dist && tsc\",\n    \"start\": \"NODE_PATH=./dist node ./dist/index.js\",\n    \"prepare\": \"husky install\",\n    \"lint\": \"eslint --max-warnings 0 . && npm run tsc\",\n    \"lint-fix\": \"eslint --fix .\",\n    \"lint-staged-husky\": \"lint-staged\",\n    \"tsc\": \"tsc --noEmit\",\n    \"format-lint\": \"prettier --config .prettierrc -l --ignore-unknown .\",\n    \"format-fix\": \"prettier --config .prettierrc --write --ignore-unknown .\",\n    \"test\": \"NODE_PATH=./src mocha --require ts-node/register 'src/**/*.test.ts' --timeout 20000 --exit\",\n    \"commit\": \"git-cz\",\n    \"docker:dev\": \"docker-compose --file docker-compose.yml --file docker-compose.dev.yml --compatibility up --build\",\n    \"docker:stage\": \"docker-compose --file docker-compose.yml --file docker-compose.stage.yml --compatibility up --build\",\n    \"docker:prod\": \"docker-compose --file docker-compose.yml --file docker-compose.prod.yml --compatibility up --build\",\n    \"migration:run\": \"NODE_PATH=./src ts-node ./node_modules/typeorm/cli.js migration:run --config ./src/orm/config/ormconfig.ts\",\n    \"migration:run:dev\": \"PG_HOST=localhost NODE_PATH=./src ts-node ./node_modules/typeorm/cli.js migration:run --config ./src/orm/config/ormconfig.ts\",\n    \"migration:revert\": \"PG_HOST=localhost NODE_PATH=./src ts-node ./node_modules/typeorm/cli.js migration:revert --config ./src/orm/config/ormconfig.ts\",\n    \"migration:generate\": \"PG_HOST=localhost NODE_PATH=./src ts-node ./node_modules/.bin/typeorm migration:generate --pretty --config ./src/orm/config/ormconfig.ts -n \",\n    \"migration:create\": \"PG_HOST=localhost NODE_PATH=./src ts-node ./node_modules/.bin/typeorm migration:create --config ./src/orm/config/ormconfig.ts -n \",\n    \"seed:run\": \"NODE_PATH=./src ts-node ./node_modules/.bin/typeorm migration:run --config ./src/orm/config/ormconfig-seed.ts\",\n    \"seed:run:dev\": \"PG_HOST=localhost NODE_PATH=./src ts-node ./node_modules/.bin/typeorm migration:run --config ./src/orm/config/ormconfig-seed.ts\",\n    \"seed:create\": \"PG_HOST=localhost NODE_PATH=./src ts-node ./node_modules/.bin/typeorm migration:create --config ./src/orm/config/ormconfig-seed.ts -n \"\n  },\n  \"dependencies\": {\n    \"bcryptjs\": \"^2.4.3\",\n    \"cors\": \"^2.8.5\",\n    \"express\": \"^4.17.3\",\n    \"helmet\": \"^5.0.2\",\n    \"jsonwebtoken\": \"^8.5.1\",\n    \"morgan\": \"^1.10.0\",\n    \"pg\": \"^8.7.3\",\n    \"reflect-metadata\": \"^0.1.13\",\n    \"typeorm\": \"^0.2.45\",\n    \"typeorm-naming-strategies\": \"^4.0.0\",\n    \"validator\": \"^13.7.0\"\n  },\n  \"devDependencies\": {\n    \"@commitlint/cli\": \"^16.2.4\",\n    \"@commitlint/config-conventional\": \"^16.2.4\",\n    \"@types/bcryptjs\": \"^2.4.2\",\n    \"@types/body-parser\": \"^1.19.2\",\n    \"@types/chai\": \"^4.3.1\",\n    \"@types/cors\": \"^2.8.12\",\n    \"@types/express\": \"^4.17.13\",\n    \"@types/jsonwebtoken\": \"^8.5.8\",\n    \"@types/mocha\": \"^9.1.1\",\n    \"@types/morgan\": \"^1.9.3\",\n    \"@types/node\": \"^17.0.33\",\n    \"@types/supertest\": \"^2.0.12\",\n    \"@types/validator\": \"^13.7.2\",\n    \"@typescript-eslint/eslint-plugin\": \"^5.23.0\",\n    \"@typescript-eslint/parser\": \"^5.23.0\",\n    \"chai\": \"^4.3.6\",\n    \"commitizen\": \"^4.2.4\",\n    \"dotenv\": \"^16.0.1\",\n    \"eslint\": \"^8.15.0\",\n    \"eslint-config-prettier\": \"^8.5.0\",\n    \"eslint-plugin-import\": \"^2.26.0\",\n    \"eslint-plugin-no-array-reduce\": \"^1.0.58\",\n    \"eslint-plugin-prettier\": \"^4.0.0\",\n    \"husky\": \"^8.0.1\",\n    \"lint-staged\": \"^12.4.1\",\n    \"mocha\": \"^9.2.2\",\n    \"nyc\": \"^15.1.0\",\n    \"prettier\": \"^2.6.2\",\n    \"pretty-quick\": \"^3.1.3\",\n    \"supertest\": \"^6.2.2\",\n    \"ts-node\": \"10.7.0\",\n    \"ts-node-dev\": \"^1.1.8\",\n    \"tsc-files\": \"^1.1.3\",\n    \"typescript\": \"4.6.4\"\n  }\n}\n"
  },
  {
    "path": "postman/RESTful_API_Boilerplate.postman_collection.json",
    "content": "{\n  \"info\": {\n    \"_postman_id\": \"da0fe49f-e424-4b96-8f55-49f7db0583dc\",\n    \"name\": \"RESTful API Boilerplate\",\n    \"schema\": \"https://schema.getpostman.com/json/collection/v2.1.0/collection.json\"\n  },\n  \"item\": [\n    {\n      \"name\": \"/auth\",\n      \"item\": [\n        {\n          \"name\": \"/login\",\n          \"event\": [\n            {\n              \"listen\": \"test\",\n              \"script\": {\n                \"id\": \"81ebd7f4-cc11-473a-a6b3-85b003f7222b\",\n                \"exec\": [\n                  \"var jsonData = JSON.parse(responseBody);\",\n                  \"\",\n                  \"postman.setEnvironmentVariable(\\\"token\\\", jsonData.data);\"\n                ],\n                \"type\": \"text/javascript\"\n              }\n            }\n          ],\n          \"request\": {\n            \"method\": \"POST\",\n            \"header\": [\n              {\n                \"key\": \"Content-Type\",\n                \"name\": \"Content-Type\",\n                \"type\": \"text\",\n                \"value\": \"application/x-www-form-urlencoded\"\n              },\n              {\n                \"key\": \"Accept-Language\",\n                \"value\": \"{{language}}\",\n                \"type\": \"text\"\n              }\n            ],\n            \"body\": {\n              \"mode\": \"urlencoded\",\n              \"urlencoded\": [\n                {\n                  \"key\": \"email\",\n                  \"value\": \"admin@admin.com\",\n                  \"type\": \"text\"\n                },\n                {\n                  \"key\": \"password\",\n                  \"value\": \"pass1\",\n                  \"type\": \"text\"\n                }\n              ]\n            },\n            \"url\": {\n              \"raw\": \"{{baseUrl}}/login\",\n              \"host\": [\"{{baseUrl}}\"],\n              \"path\": [\"login\"]\n            }\n          },\n          \"response\": []\n        },\n        {\n          \"name\": \"/register\",\n          \"request\": {\n            \"method\": \"POST\",\n            \"header\": [\n              {\n                \"key\": \"Content-Type\",\n                \"name\": \"Content-Type\",\n                \"value\": \"application/x-www-form-urlencoded\",\n                \"type\": \"text\"\n              },\n              {\n                \"key\": \"Accept-Language\",\n                \"value\": \"{{language}}\",\n                \"type\": \"text\"\n              }\n            ],\n            \"body\": {\n              \"mode\": \"urlencoded\",\n              \"urlencoded\": [\n                {\n                  \"key\": \"email\",\n                  \"value\": \"admin1@admin.com\",\n                  \"type\": \"text\"\n                },\n                {\n                  \"key\": \"password\",\n                  \"value\": \"admin\",\n                  \"type\": \"text\"\n                },\n                {\n                  \"key\": \"passwordConfirm\",\n                  \"value\": \"admin\",\n                  \"type\": \"text\"\n                }\n              ]\n            },\n            \"url\": {\n              \"raw\": \"{{baseUrl}}/register\",\n              \"host\": [\"{{baseUrl}}\"],\n              \"path\": [\"register\"]\n            }\n          },\n          \"response\": []\n        },\n        {\n          \"name\": \"/change-password\",\n          \"request\": {\n            \"method\": \"POST\",\n            \"header\": [\n              {\n                \"key\": \"Content-Type\",\n                \"name\": \"Content-Type\",\n                \"type\": \"text\",\n                \"value\": \"application/x-www-form-urlencoded\"\n              },\n              {\n                \"key\": \"Authorization\",\n                \"value\": \"{{token}}\",\n                \"type\": \"text\"\n              },\n              {\n                \"key\": \"Accept-Language\",\n                \"value\": \"{{language}}\",\n                \"type\": \"text\"\n              }\n            ],\n            \"body\": {\n              \"mode\": \"urlencoded\",\n              \"urlencoded\": [\n                {\n                  \"key\": \"password\",\n                  \"value\": \"admin\",\n                  \"type\": \"text\"\n                },\n                {\n                  \"key\": \"passwordNew\",\n                  \"value\": \"admin\",\n                  \"type\": \"text\"\n                },\n                {\n                  \"key\": \"passwordConfirm\",\n                  \"value\": \"admin\",\n                  \"type\": \"text\"\n                }\n              ]\n            },\n            \"url\": {\n              \"raw\": \"{{baseUrl}}/change-password\",\n              \"host\": [\"{{baseUrl}}\"],\n              \"path\": [\"change-password\"]\n            }\n          },\n          \"response\": []\n        }\n      ],\n      \"protocolProfileBehavior\": {}\n    },\n    {\n      \"name\": \"/misc\",\n      \"item\": [\n        {\n          \"name\": \"/change-language\",\n          \"request\": {\n            \"method\": \"POST\",\n            \"header\": [\n              {\n                \"key\": \"Content-Type\",\n                \"name\": \"Content-Type\",\n                \"type\": \"text\",\n                \"value\": \"application/x-www-form-urlencoded\"\n              },\n              {\n                \"key\": \"Accept-Language\",\n                \"type\": \"text\",\n                \"value\": \"sl-SI\"\n              }\n            ],\n            \"body\": {\n              \"mode\": \"urlencoded\",\n              \"urlencoded\": [\n                {\n                  \"key\": \"language\",\n                  \"value\": \"sl-SI\",\n                  \"type\": \"text\"\n                }\n              ]\n            },\n            \"url\": {\n              \"raw\": \"{{baseUrl}}/misc/change-language\",\n              \"host\": [\"{{baseUrl}}\"],\n              \"path\": [\"misc\", \"change-language\"]\n            }\n          },\n          \"response\": []\n        }\n      ],\n      \"protocolProfileBehavior\": {}\n    },\n    {\n      \"name\": \"/users\",\n      \"item\": [\n        {\n          \"name\": \"/\",\n          \"protocolProfileBehavior\": {\n            \"disableBodyPruning\": true\n          },\n          \"request\": {\n            \"method\": \"GET\",\n            \"header\": [\n              {\n                \"key\": \"Content-Type\",\n                \"name\": \"Content-Type\",\n                \"type\": \"text\",\n                \"value\": \"application/x-www-form-urlencoded\"\n              },\n              {\n                \"key\": \"Accept-Language\",\n                \"type\": \"text\",\n                \"value\": \"sl-SI\"\n              },\n              {\n                \"key\": \"Authorization\",\n                \"value\": \"{{token}}\",\n                \"type\": \"text\"\n              }\n            ],\n            \"body\": {\n              \"mode\": \"urlencoded\",\n              \"urlencoded\": []\n            },\n            \"url\": {\n              \"raw\": \"{{baseUrl}}/users\",\n              \"host\": [\"{{baseUrl}}\"],\n              \"path\": [\"users\"]\n            }\n          },\n          \"response\": []\n        },\n        {\n          \"name\": \"/:id\",\n          \"protocolProfileBehavior\": {\n            \"disableBodyPruning\": true\n          },\n          \"request\": {\n            \"method\": \"GET\",\n            \"header\": [\n              {\n                \"key\": \"Content-Type\",\n                \"name\": \"Content-Type\",\n                \"type\": \"text\",\n                \"value\": \"application/x-www-form-urlencoded\"\n              },\n              {\n                \"key\": \"Accept-Language\",\n                \"type\": \"text\",\n                \"value\": \"sl-SI\"\n              },\n              {\n                \"key\": \"Authorization\",\n                \"type\": \"text\",\n                \"value\": \"{{token}}\"\n              }\n            ],\n            \"body\": {\n              \"mode\": \"urlencoded\",\n              \"urlencoded\": []\n            },\n            \"url\": {\n              \"raw\": \"{{baseUrl}}/users/3\",\n              \"host\": [\"{{baseUrl}}\"],\n              \"path\": [\"users\", \"3\"]\n            }\n          },\n          \"response\": []\n        },\n        {\n          \"name\": \"/:id\",\n          \"request\": {\n            \"method\": \"DELETE\",\n            \"header\": [\n              {\n                \"key\": \"Content-Type\",\n                \"name\": \"Content-Type\",\n                \"type\": \"text\",\n                \"value\": \"application/x-www-form-urlencoded\"\n              },\n              {\n                \"key\": \"Accept-Language\",\n                \"type\": \"text\",\n                \"value\": \"sl-SI\"\n              },\n              {\n                \"key\": \"Authorization\",\n                \"type\": \"text\",\n                \"value\": \"{{token}}\"\n              }\n            ],\n            \"body\": {\n              \"mode\": \"urlencoded\",\n              \"urlencoded\": []\n            },\n            \"url\": {\n              \"raw\": \"{{baseUrl}}/users/9\",\n              \"host\": [\"{{baseUrl}}\"],\n              \"path\": [\"users\", \"9\"]\n            }\n          },\n          \"response\": []\n        },\n        {\n          \"name\": \"/:id\",\n          \"request\": {\n            \"method\": \"PATCH\",\n            \"header\": [\n              {\n                \"key\": \"Content-Type\",\n                \"name\": \"Content-Type\",\n                \"type\": \"text\",\n                \"value\": \"application/x-www-form-urlencoded\"\n              },\n              {\n                \"key\": \"Accept-Language\",\n                \"type\": \"text\",\n                \"value\": \"sl-SI\"\n              },\n              {\n                \"key\": \"Authorization\",\n                \"type\": \"text\",\n                \"value\": \"{{token}}\"\n              }\n            ],\n            \"body\": {\n              \"mode\": \"urlencoded\",\n              \"urlencoded\": [\n                {\n                  \"key\": \"username\",\n                  \"value\": \"Tyrion1\",\n                  \"type\": \"text\"\n                },\n                {\n                  \"key\": \"name\",\n                  \"value\": \"test name\",\n                  \"type\": \"text\"\n                }\n              ]\n            },\n            \"url\": {\n              \"raw\": \"{{baseUrl}}/users/5\",\n              \"host\": [\"{{baseUrl}}\"],\n              \"path\": [\"users\", \"5\"]\n            }\n          },\n          \"response\": []\n        }\n      ],\n      \"protocolProfileBehavior\": {}\n    }\n  ],\n  \"protocolProfileBehavior\": {}\n}\n"
  },
  {
    "path": "postman/RESTful_API_Boilerplate.postman_environment.json",
    "content": "{\n  \"id\": \"a8df1c58-9c26-4452-a61e-0f0d2e865ee1\",\n  \"name\": \"RESTful API Boilerplate\",\n  \"values\": [\n    {\n      \"key\": \"host\",\n      \"value\": \"http://localhost:4000\",\n      \"enabled\": true\n    },\n    {\n      \"key\": \"baseUrl\",\n      \"value\": \"{{host}}\",\n      \"enabled\": true\n    },\n    {\n      \"key\": \"language\",\n      \"value\": \"en\",\n      \"enabled\": true\n    },\n    {\n      \"key\": \"token\",\n      \"value\": \"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwibmFtZSI6IldhbHRlciBXaGl0ZSIsImVtYWlsIjoiYWRtaW5AYWRtaW4uY29tIiwicm9sZSI6IkFETUlOSVNUUkFUT1IiLCJjcmVhdGVkX2F0IjoiMjAyMC0wNi0xM1QwODoyNjoxNS41MjZaIiwiaWF0IjoxNTkyMTYxMjcwLCJleHAiOjE1OTIxNjIxNzB9.Fi-jszKGqt2plCqJj9jCM5x5l0KSQVkuQu9-qqElMug\",\n      \"enabled\": true\n    }\n  ],\n  \"_postman_variable_scope\": \"environment\",\n  \"_postman_exported_at\": \"2020-06-14T19:04:35.409Z\",\n  \"_postman_exported_using\": \"Postman/7.26.0\"\n}\n"
  },
  {
    "path": "renovate.json",
    "content": "{\n  \"extends\": [\"config:base\", \":disableDependencyDashboard\"],\n  \"separateMinorPatch\": true,\n  \"assignees\": [\"mkosir\"],\n  \"assignAutomerge\": true,\n  \"requiredStatusChecks\": null,\n  \"rangeStrategy\": \"bump\",\n  \"enabled\": false,\n  \"packageRules\": [\n    {\n      \"matchUpdateTypes\": [\"minor\", \"major\"],\n      \"enabled\": false\n    },\n    {\n      \"automerge\": true,\n      \"labels\": [\"automerge\", \"dependencies\", \"patch\"],\n      \"groupName\": \"group:dependencies\",\n      \"matchDepTypes\": [\"dependencies\"]\n    },\n    {\n      \"automerge\": true,\n      \"labels\": [\"automerge\", \"devDependencies\", \"patch\"],\n      \"groupName\": \"group:devDependencies\",\n      \"matchDepTypes\": [\"devDependencies\"]\n    }\n  ]\n}\n"
  },
  {
    "path": "scripts/be-node-dev.sh",
    "content": "#!/bin/sh\n\necho \"Install bash and execute 'wait-for-it.sh' script\"\napk add --update bash\n./scripts/wait-for-it.sh $PG_HOST:5432 --timeout=30 --strict -- echo \"postgres up and running\"\n\nnpm run migration:run\nnpm run seed:run\nnpm run dev"
  },
  {
    "path": "scripts/wait-for-it.sh",
    "content": "#!/usr/bin/env bash\n\nWAITFORIT_cmdname=${0##*/}\n\nechoerr() { if [[ $WAITFORIT_QUIET -ne 1 ]]; then echo \"$@\" 1>&2; fi }\n\nusage()\n{\n    cat << USAGE >&2\nUsage:\n    $WAITFORIT_cmdname host:port [-s] [-t timeout] [-- command args]\n    -h HOST | --host=HOST       Host or IP under test\n    -p PORT | --port=PORT       TCP port under test\n                                Alternatively, you specify the host and port as host:port\n    -s | --strict               Only execute subcommand if the test succeeds\n    -q | --quiet                Don't output any status messages\n    -t TIMEOUT | --timeout=TIMEOUT\n                                Timeout in seconds, zero for no timeout\n    -- COMMAND ARGS             Execute command with args after the test finishes\nUSAGE\n    exit 1\n}\n\nwait_for()\n{\n    if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then\n        echoerr \"$WAITFORIT_cmdname: waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT\"\n    else\n        echoerr \"$WAITFORIT_cmdname: waiting for $WAITFORIT_HOST:$WAITFORIT_PORT without a timeout\"\n    fi\n    WAITFORIT_start_ts=$(date +%s)\n    while :\n    do\n        if [[ $WAITFORIT_ISBUSY -eq 1 ]]; then\n            nc -z $WAITFORIT_HOST $WAITFORIT_PORT\n            WAITFORIT_result=$?\n        else\n            (echo > /dev/tcp/$WAITFORIT_HOST/$WAITFORIT_PORT) >/dev/null 2>&1\n            WAITFORIT_result=$?\n        fi\n        if [[ $WAITFORIT_result -eq 0 ]]; then\n            WAITFORIT_end_ts=$(date +%s)\n            echoerr \"$WAITFORIT_cmdname: $WAITFORIT_HOST:$WAITFORIT_PORT is available after $((WAITFORIT_end_ts - WAITFORIT_start_ts)) seconds\"\n            break\n        fi\n        sleep 1\n    done\n    return $WAITFORIT_result\n}\n\nwait_for_wrapper()\n{\n    if [[ $WAITFORIT_QUIET -eq 1 ]]; then\n        timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --quiet --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT &\n    else\n        timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT &\n    fi\n    WAITFORIT_PID=$!\n    trap \"kill -INT -$WAITFORIT_PID\" INT\n    wait $WAITFORIT_PID\n    WAITFORIT_RESULT=$?\n    if [[ $WAITFORIT_RESULT -ne 0 ]]; then\n        echoerr \"$WAITFORIT_cmdname: timeout occurred after waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT\"\n    fi\n    return $WAITFORIT_RESULT\n}\n\n# process arguments\nwhile [[ $# -gt 0 ]]\ndo\n    case \"$1\" in\n        *:* )\n        WAITFORIT_hostport=(${1//:/ })\n        WAITFORIT_HOST=${WAITFORIT_hostport[0]}\n        WAITFORIT_PORT=${WAITFORIT_hostport[1]}\n        shift 1\n        ;;\n        --child)\n        WAITFORIT_CHILD=1\n        shift 1\n        ;;\n        -q | --quiet)\n        WAITFORIT_QUIET=1\n        shift 1\n        ;;\n        -s | --strict)\n        WAITFORIT_STRICT=1\n        shift 1\n        ;;\n        -h)\n        WAITFORIT_HOST=\"$2\"\n        if [[ $WAITFORIT_HOST == \"\" ]]; then break; fi\n        shift 2\n        ;;\n        --host=*)\n        WAITFORIT_HOST=\"${1#*=}\"\n        shift 1\n        ;;\n        -p)\n        WAITFORIT_PORT=\"$2\"\n        if [[ $WAITFORIT_PORT == \"\" ]]; then break; fi\n        shift 2\n        ;;\n        --port=*)\n        WAITFORIT_PORT=\"${1#*=}\"\n        shift 1\n        ;;\n        -t)\n        WAITFORIT_TIMEOUT=\"$2\"\n        if [[ $WAITFORIT_TIMEOUT == \"\" ]]; then break; fi\n        shift 2\n        ;;\n        --timeout=*)\n        WAITFORIT_TIMEOUT=\"${1#*=}\"\n        shift 1\n        ;;\n        --)\n        shift\n        WAITFORIT_CLI=(\"$@\")\n        break\n        ;;\n        --help)\n        usage\n        ;;\n        *)\n        echoerr \"Unknown argument: $1\"\n        usage\n        ;;\n    esac\ndone\n\nif [[ \"$WAITFORIT_HOST\" == \"\" || \"$WAITFORIT_PORT\" == \"\" ]]; then\n    echoerr \"Error: you need to provide a host and port to test.\"\n    usage\nfi\n\nWAITFORIT_TIMEOUT=${WAITFORIT_TIMEOUT:-15}\nWAITFORIT_STRICT=${WAITFORIT_STRICT:-0}\nWAITFORIT_CHILD=${WAITFORIT_CHILD:-0}\nWAITFORIT_QUIET=${WAITFORIT_QUIET:-0}\n\n# Check to see if timeout is from busybox?\nWAITFORIT_TIMEOUT_PATH=$(type -p timeout)\nWAITFORIT_TIMEOUT_PATH=$(realpath $WAITFORIT_TIMEOUT_PATH 2>/dev/null || readlink -f $WAITFORIT_TIMEOUT_PATH)\n\nWAITFORIT_BUSYTIMEFLAG=\"\"\nif [[ $WAITFORIT_TIMEOUT_PATH =~ \"busybox\" ]]; then\n    WAITFORIT_ISBUSY=1\n    # Check if busybox timeout uses -t flag\n    # (recent Alpine versions don't support -t anymore)\n    if timeout &>/dev/stdout | grep -q -e '-t '; then\n        WAITFORIT_BUSYTIMEFLAG=\"-t\"\n    fi\nelse\n    WAITFORIT_ISBUSY=0\nfi\n\nif [[ $WAITFORIT_CHILD -gt 0 ]]; then\n    wait_for\n    WAITFORIT_RESULT=$?\n    exit $WAITFORIT_RESULT\nelse\n    if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then\n        wait_for_wrapper\n        WAITFORIT_RESULT=$?\n    else\n        wait_for\n        WAITFORIT_RESULT=$?\n    fi\nfi\n\nif [[ $WAITFORIT_CLI != \"\" ]]; then\n    if [[ $WAITFORIT_RESULT -ne 0 && $WAITFORIT_STRICT -eq 1 ]]; then\n        echoerr \"$WAITFORIT_cmdname: strict mode, refusing to execute subprocess\"\n        exit $WAITFORIT_RESULT\n    fi\n    exec \"${WAITFORIT_CLI[@]}\"\nelse\n    exit $WAITFORIT_RESULT\nfi"
  },
  {
    "path": "src/consts/ConstsUser.ts",
    "content": "export enum ConstsUser {\n  PASSWORD_MIN_CHAR = 4,\n}\n"
  },
  {
    "path": "src/controllers/auth/changePassword.ts",
    "content": "import { Request, Response, NextFunction } from 'express';\nimport { getRepository } from 'typeorm';\n\nimport { User } from 'orm/entities/users/User';\nimport { CustomError } from 'utils/response/custom-error/CustomError';\n\nexport const changePassword = async (req: Request, res: Response, next: NextFunction) => {\n  const { password, passwordNew } = req.body;\n  const { id, name } = req.jwtPayload;\n\n  const userRepository = getRepository(User);\n  try {\n    const user = await userRepository.findOne({ where: { id } });\n\n    if (!user) {\n      const customError = new CustomError(404, 'General', 'Not Found', [`User ${name} not found.`]);\n      return next(customError);\n    }\n\n    if (!user.checkIfPasswordMatch(password)) {\n      const customError = new CustomError(400, 'General', 'Not Found', ['Incorrect password']);\n      return next(customError);\n    }\n\n    user.password = passwordNew;\n    user.hashPassword();\n    userRepository.save(user);\n\n    res.customSuccess(200, 'Password successfully changed.');\n  } catch (err) {\n    const customError = new CustomError(400, 'Raw', 'Error', null, err);\n    return next(customError);\n  }\n};\n"
  },
  {
    "path": "src/controllers/auth/index.ts",
    "content": "export * from './changePassword';\nexport * from './login';\nexport * from './register';\n"
  },
  {
    "path": "src/controllers/auth/login.test.ts",
    "content": "import 'mocha';\nimport { expect } from 'chai';\nimport { agent as request } from 'supertest';\nimport { getRepository, Connection, Repository } from 'typeorm';\n\nimport { dbCreateConnection } from 'orm/dbCreateConnection';\nimport { Role } from 'orm/entities/users/types';\nimport { User } from 'orm/entities/users/User';\n\nimport { app } from '../../';\n\ndescribe('Login', () => {\n  let dbConnection: Connection;\n  let userRepository: Repository<User>;\n\n  const userPassword = 'pass1';\n  const user = new User();\n  user.username = 'Badger';\n  user.name = 'Brandon Mayhew';\n  user.email = 'brandon.mayhew@test.com';\n  user.password = userPassword;\n  user.hashPassword();\n  user.role = 'ADMINISTRATOR' as Role;\n\n  before(async () => {\n    dbConnection = await dbCreateConnection();\n    userRepository = getRepository(User);\n  });\n\n  beforeEach(async () => {\n    await userRepository.save(user);\n  });\n\n  afterEach(async () => {\n    await userRepository.delete(user.id);\n  });\n\n  it('should return a JWT token', async () => {\n    const res = await request(app).post('/v1/auth/login').send({ email: user.email, password: userPassword });\n    expect(res.status).to.equal(200);\n    expect(res.body.message).to.equal('Token successfully created.');\n    expect(res.body.data).not.to.be.empty;\n    expect(res.body.data).to.be.an('string');\n  });\n\n  it(\"should report error when email and password don't match\", async () => {\n    const res = await request(app).post('/v1/auth/login').send({ email: user.email, password: 'wrong_password' });\n    expect(res.status).to.equal(404);\n    expect(res.body.errorType).to.equal('General');\n    expect(res.body.errors).to.eql(['Incorrect email or password']);\n    expect(res.body.errorRaw).to.an('null');\n    expect(res.body.errorsValidation).to.an('null');\n  });\n\n  it('should report error when the email provided is not valid', async () => {\n    const res = await request(app).post('/v1/auth/login').send({ email: 'not_valid_email', password: userPassword });\n    expect(res.status).to.equal(400);\n    expect(res.body.errorType).to.equal('Validation');\n    expect(res.body.errorMessage).to.equal('Login validation error');\n    expect(res.body.errors).to.an('null');\n    expect(res.body.errorRaw).to.an('null');\n    expect(res.body.errorsValidation).to.eql([\n      {\n        email: 'Email is invalid',\n      },\n    ]);\n  });\n});\n"
  },
  {
    "path": "src/controllers/auth/login.ts",
    "content": "import { Request, Response, NextFunction } from 'express';\nimport { getRepository } from 'typeorm';\n\nimport { Role } from 'orm/entities/users/types';\nimport { User } from 'orm/entities/users/User';\nimport { JwtPayload } from 'types/JwtPayload';\nimport { createJwtToken } from 'utils/createJwtToken';\nimport { CustomError } from 'utils/response/custom-error/CustomError';\n\nexport const login = async (req: Request, res: Response, next: NextFunction) => {\n  const { email, password } = req.body;\n\n  const userRepository = getRepository(User);\n  try {\n    const user = await userRepository.findOne({ where: { email } });\n\n    if (!user) {\n      const customError = new CustomError(404, 'General', 'Not Found', ['Incorrect email or password']);\n      return next(customError);\n    }\n\n    if (!user.checkIfPasswordMatch(password)) {\n      const customError = new CustomError(404, 'General', 'Not Found', ['Incorrect email or password']);\n      return next(customError);\n    }\n\n    const jwtPayload: JwtPayload = {\n      id: user.id,\n      name: user.name,\n      email: user.email,\n      role: user.role as Role,\n      created_at: user.created_at,\n    };\n\n    try {\n      const token = createJwtToken(jwtPayload);\n      res.customSuccess(200, 'Token successfully created.', `Bearer ${token}`);\n    } catch (err) {\n      const customError = new CustomError(400, 'Raw', \"Token can't be created\", null, err);\n      return next(customError);\n    }\n  } catch (err) {\n    const customError = new CustomError(400, 'Raw', 'Error', null, err);\n    return next(customError);\n  }\n};\n"
  },
  {
    "path": "src/controllers/auth/register.test.ts",
    "content": "import 'mocha';\nimport { expect } from 'chai';\nimport { agent as request } from 'supertest';\nimport { getRepository, Connection, Repository } from 'typeorm';\n\nimport { dbCreateConnection } from 'orm/dbCreateConnection';\nimport { User } from 'orm/entities/users/User';\n\nimport { app } from '../../';\n\ndescribe('Register', () => {\n  let dbConnection: Connection;\n  let userRepository: Repository<User>;\n\n  const userPassword = 'pass1';\n  const user = new User();\n  user.email = 'brandon.mayhew@test.com';\n  user.password = userPassword;\n  user.hashPassword();\n\n  before(async () => {\n    dbConnection = await dbCreateConnection();\n    userRepository = getRepository(User);\n  });\n\n  it('should register a new user', async () => {\n    const res = await request(app)\n      .post('/v1/auth/register')\n      .send({ email: user.email, password: userPassword, passwordConfirm: userPassword });\n    expect(res.status).to.equal(200);\n    expect(res.body.message).to.equal('User successfully created.');\n    expect(res.body.data).to.be.an('null');\n    await userRepository.delete({ email: user.email });\n  });\n\n  it('should report error when email already exists', async () => {\n    let res = await request(app)\n      .post('/v1/auth/register')\n      .send({ email: user.email, password: userPassword, passwordConfirm: userPassword });\n    res = await request(app)\n      .post('/v1/auth/register')\n      .send({ email: user.email, password: userPassword, passwordConfirm: userPassword });\n    expect(res.status).to.equal(400);\n    expect(res.body.errorType).to.equal('General');\n    expect(res.body.errorMessage).to.equal('User already exists');\n    expect(res.body.errors).to.eql([`Email '${user.email}' already exists`]);\n    expect(res.body.errorRaw).to.an('null');\n    expect(res.body.errorsValidation).to.an('null');\n    await userRepository.delete({ email: user.email });\n  });\n});\n"
  },
  {
    "path": "src/controllers/auth/register.ts",
    "content": "import { Request, Response, NextFunction } from 'express';\nimport { getRepository } from 'typeorm';\n\nimport { User } from 'orm/entities/users/User';\nimport { CustomError } from 'utils/response/custom-error/CustomError';\n\nexport const register = async (req: Request, res: Response, next: NextFunction) => {\n  const { email, password } = req.body;\n\n  const userRepository = getRepository(User);\n  try {\n    const user = await userRepository.findOne({ where: { email } });\n\n    if (user) {\n      const customError = new CustomError(400, 'General', 'User already exists', [\n        `Email '${user.email}' already exists`,\n      ]);\n      return next(customError);\n    }\n\n    try {\n      const newUser = new User();\n      newUser.email = email;\n      newUser.password = password;\n      newUser.hashPassword();\n      await userRepository.save(newUser);\n\n      res.customSuccess(200, 'User successfully created.');\n    } catch (err) {\n      const customError = new CustomError(400, 'Raw', `User '${email}' can't be created`, null, err);\n      return next(customError);\n    }\n  } catch (err) {\n    const customError = new CustomError(400, 'Raw', 'Error', null, err);\n    return next(customError);\n  }\n};\n"
  },
  {
    "path": "src/controllers/users/destroy.ts",
    "content": "import { Request, Response, NextFunction } from 'express';\nimport { getRepository } from 'typeorm';\n\nimport { User } from 'orm/entities/users/User';\nimport { CustomError } from 'utils/response/custom-error/CustomError';\n\nexport const destroy = async (req: Request, res: Response, next: NextFunction) => {\n  const id = req.params.id;\n\n  const userRepository = getRepository(User);\n  try {\n    const user = await userRepository.findOne({ where: { id } });\n\n    if (!user) {\n      const customError = new CustomError(404, 'General', 'Not Found', [`User with id:${id} doesn't exists.`]);\n      return next(customError);\n    }\n    userRepository.delete(id);\n\n    res.customSuccess(200, 'User successfully deleted.', { id: user.id, name: user.name, email: user.email });\n  } catch (err) {\n    const customError = new CustomError(400, 'Raw', 'Error', null, err);\n    return next(customError);\n  }\n};\n"
  },
  {
    "path": "src/controllers/users/edit.ts",
    "content": "import { Request, Response, NextFunction } from 'express';\nimport { getRepository } from 'typeorm';\n\nimport { User } from 'orm/entities/users/User';\nimport { CustomError } from 'utils/response/custom-error/CustomError';\n\nexport const edit = async (req: Request, res: Response, next: NextFunction) => {\n  const id = req.params.id;\n  const { username, name } = req.body;\n\n  const userRepository = getRepository(User);\n  try {\n    const user = await userRepository.findOne({ where: { id } });\n\n    if (!user) {\n      const customError = new CustomError(404, 'General', `User with id:${id} not found.`, ['User not found.']);\n      return next(customError);\n    }\n\n    user.username = username;\n    user.name = name;\n\n    try {\n      await userRepository.save(user);\n      res.customSuccess(200, 'User successfully saved.');\n    } catch (err) {\n      const customError = new CustomError(409, 'Raw', `User '${user.email}' can't be saved.`, null, err);\n      return next(customError);\n    }\n  } catch (err) {\n    const customError = new CustomError(400, 'Raw', 'Error', null, err);\n    return next(customError);\n  }\n};\n"
  },
  {
    "path": "src/controllers/users/index.test.ts",
    "content": "import 'mocha';\nimport { expect } from 'chai';\nimport { agent as request } from 'supertest';\nimport { getRepository, Connection, Repository } from 'typeorm';\n\nimport { dbCreateConnection } from 'orm/dbCreateConnection';\nimport { Role } from 'orm/entities/users/types';\nimport { User } from 'orm/entities/users/User';\n\nimport { app } from '../../';\n\ndescribe('Users', () => {\n  let dbConnection: Connection;\n  let userRepository: Repository<User>;\n\n  const userPassword = 'pass1';\n  let adminUserToken = null;\n  const adminUser = new User();\n  adminUser.username = 'Badger';\n  adminUser.name = 'Brandon Mayhew';\n  adminUser.email = 'brandon.mayhew@test.com';\n  adminUser.password = userPassword;\n  adminUser.hashPassword();\n  adminUser.role = 'ADMINISTRATOR' as Role;\n\n  let standardUserToken = null;\n  const standardUser = new User();\n  standardUser.username = 'Toddy';\n  standardUser.name = 'Todd Alquist';\n  standardUser.email = 'todd.alquist@test.com';\n  standardUser.password = userPassword;\n  standardUser.hashPassword();\n  standardUser.role = 'STANDARD' as Role;\n\n  before(async () => {\n    dbConnection = await dbCreateConnection();\n    userRepository = getRepository(User);\n  });\n\n  beforeEach(async () => {\n    await userRepository.save([adminUser, standardUser]);\n    let res = await request(app).post('/v1/auth/login').send({ email: adminUser.email, password: userPassword });\n    adminUserToken = res.body.data;\n    res = await request(app).post('/v1/auth/login').send({ email: standardUser.email, password: userPassword });\n    standardUserToken = res.body.data;\n  });\n\n  afterEach(async () => {\n    await userRepository.delete([adminUser.id, standardUser.id]);\n  });\n\n  describe('GET /v1/auth/users', () => {\n    it('should get all users', async () => {\n      const res = await request(app).get('/v1/users').set('Authorization', adminUserToken);\n      expect(res.status).to.equal(200);\n      expect(res.body.message).to.equal('List of users.');\n      expect(res.body.data[3].email).to.eql('hank.schrader@test.com');\n    });\n\n    it('should report error of unauthorized user', async () => {\n      const res = await request(app).get('/v1/users').set('Authorization', standardUserToken);\n      expect(res.status).to.equal(401);\n      expect(res.body.errorType).to.equal('Unauthorized');\n      expect(res.body.errorMessage).to.equal('Unauthorized - Insufficient user rights');\n      expect(res.body.errors).to.eql([\n        'Unauthorized - Insufficient user rights',\n        'Current role: STANDARD. Required role: ADMINISTRATOR',\n      ]);\n      expect(res.body.errorRaw).to.an('null');\n      expect(res.body.errorsValidation).to.an('null');\n    });\n  });\n\n  describe('GET /v1/auth/users//:id([0-9]+)', () => {\n    it('should get user', async () => {\n      const user = await userRepository.findOne({ email: adminUser.email });\n      const res = await request(app).get(`/v1/users/${user.id}`).set('Authorization', adminUserToken);\n      expect(res.status).to.equal(200);\n      expect(res.body.message).to.equal('User found');\n      expect(res.body.data.email).to.eql(adminUser.email);\n    });\n  });\n});\n"
  },
  {
    "path": "src/controllers/users/index.ts",
    "content": "export * from './destroy';\nexport * from './edit';\nexport * from './list';\nexport * from './show';\n"
  },
  {
    "path": "src/controllers/users/list.ts",
    "content": "import { Request, Response, NextFunction } from 'express';\nimport { getRepository } from 'typeorm';\n\nimport { User } from 'orm/entities/users/User';\nimport { CustomError } from 'utils/response/custom-error/CustomError';\n\nexport const list = async (req: Request, res: Response, next: NextFunction) => {\n  const userRepository = getRepository(User);\n  try {\n    const users = await userRepository.find({\n      select: ['id', 'username', 'name', 'email', 'role', 'language', 'created_at', 'updated_at'],\n    });\n    res.customSuccess(200, 'List of users.', users);\n  } catch (err) {\n    const customError = new CustomError(400, 'Raw', `Can't retrieve list of users.`, null, err);\n    return next(customError);\n  }\n};\n"
  },
  {
    "path": "src/controllers/users/show.ts",
    "content": "import { Request, Response, NextFunction } from 'express';\nimport { getRepository } from 'typeorm';\n\nimport { User } from 'orm/entities/users/User';\nimport { CustomError } from 'utils/response/custom-error/CustomError';\n\nexport const show = async (req: Request, res: Response, next: NextFunction) => {\n  const id = req.params.id;\n\n  const userRepository = getRepository(User);\n  try {\n    const user = await userRepository.findOne(id, {\n      select: ['id', 'username', 'name', 'email', 'role', 'language', 'created_at', 'updated_at'],\n    });\n\n    if (!user) {\n      const customError = new CustomError(404, 'General', `User with id:${id} not found.`, ['User not found.']);\n      return next(customError);\n    }\n    res.customSuccess(200, 'User found', user);\n  } catch (err) {\n    const customError = new CustomError(400, 'Raw', 'Error', null, err);\n    return next(customError);\n  }\n};\n"
  },
  {
    "path": "src/index.ts",
    "content": "import 'dotenv/config';\nimport 'reflect-metadata';\nimport fs from 'fs';\nimport path from 'path';\n\nimport bodyParser from 'body-parser';\nimport cors from 'cors';\nimport express from 'express';\nimport helmet from 'helmet';\nimport morgan from 'morgan';\n\nimport './utils/response/customSuccess';\nimport { errorHandler } from './middleware/errorHandler';\nimport { getLanguage } from './middleware/getLanguage';\nimport { dbCreateConnection } from './orm/dbCreateConnection';\nimport routes from './routes';\n\nexport const app = express();\napp.use(cors());\napp.use(helmet());\napp.use(bodyParser.json());\napp.use(bodyParser.urlencoded({ extended: false }));\napp.use(getLanguage);\n\ntry {\n  const accessLogStream = fs.createWriteStream(path.join(__dirname, '../log/access.log'), {\n    flags: 'a',\n  });\n  app.use(morgan('combined', { stream: accessLogStream }));\n} catch (err) {\n  console.log(err);\n}\napp.use(morgan('combined'));\n\napp.use('/', routes);\n\napp.use(errorHandler);\n\nconst port = process.env.PORT || 4000;\napp.listen(port, () => {\n  console.log(`Server running on port ${port}`);\n});\n\n(async () => {\n  await dbCreateConnection();\n})();\n"
  },
  {
    "path": "src/middleware/checkJwt.ts",
    "content": "import { Request, Response, NextFunction } from 'express';\nimport jwt from 'jsonwebtoken';\n\nimport { JwtPayload } from '../types/JwtPayload';\nimport { createJwtToken } from '../utils/createJwtToken';\nimport { CustomError } from '../utils/response/custom-error/CustomError';\n\nexport const checkJwt = (req: Request, res: Response, next: NextFunction) => {\n  const authHeader = req.get('Authorization');\n  if (!authHeader) {\n    const customError = new CustomError(400, 'General', 'Authorization header not provided');\n    return next(customError);\n  }\n\n  const token = authHeader.split(' ')[1];\n  let jwtPayload: { [key: string]: any };\n  try {\n    jwtPayload = jwt.verify(token, process.env.JWT_SECRET as string) as { [key: string]: any };\n    ['iat', 'exp'].forEach((keyToRemove) => delete jwtPayload[keyToRemove]);\n    req.jwtPayload = jwtPayload as JwtPayload;\n  } catch (err) {\n    const customError = new CustomError(401, 'Raw', 'JWT error', null, err);\n    return next(customError);\n  }\n\n  try {\n    // Refresh and send a new token on every request\n    const newToken = createJwtToken(jwtPayload as JwtPayload);\n    res.setHeader('token', `Bearer ${newToken}`);\n    return next();\n  } catch (err) {\n    const customError = new CustomError(400, 'Raw', \"Token can't be created\", null, err);\n    return next(customError);\n  }\n};\n"
  },
  {
    "path": "src/middleware/checkRole.ts",
    "content": "import { Request, Response, NextFunction } from 'express';\n\nimport { Role } from '../orm/entities/users/types';\nimport { CustomError } from '../utils/response/custom-error/CustomError';\n\nexport const checkRole = (roles: Role[], isSelfAllowed = false) => {\n  return async (req: Request, res: Response, next: NextFunction) => {\n    const { id, role } = req.jwtPayload;\n    const { id: requestId } = req.params;\n\n    let errorSelfAllowed: string | null = null;\n    if (isSelfAllowed) {\n      if (id === parseInt(requestId)) {\n        return next();\n      }\n      errorSelfAllowed = 'Self allowed action.';\n    }\n\n    if (roles.indexOf(role) === -1) {\n      const errors = [\n        'Unauthorized - Insufficient user rights',\n        `Current role: ${role}. Required role: ${roles.toString()}`,\n      ];\n      if (errorSelfAllowed) {\n        errors.push(errorSelfAllowed);\n      }\n      const customError = new CustomError(401, 'Unauthorized', 'Unauthorized - Insufficient user rights', errors);\n      return next(customError);\n    }\n    return next();\n  };\n};\n"
  },
  {
    "path": "src/middleware/errorHandler.ts",
    "content": "import { Request, Response, NextFunction } from 'express';\n\nimport { CustomError } from '../utils/response/custom-error/CustomError';\n\nexport const errorHandler = (err: CustomError, req: Request, res: Response, next: NextFunction) => {\n  return res.status(err.HttpStatusCode).json(err.JSON);\n};\n"
  },
  {
    "path": "src/middleware/getLanguage.ts",
    "content": "import { Request, Response, NextFunction } from 'express';\n\nimport { Language } from '../orm/entities/users/types';\n\nexport const getLanguage = (req: Request, res: Response, next: NextFunction) => {\n  const acceptLanguageHeader = req.get('Accept-Language') as Language | null;\n  if (!acceptLanguageHeader) {\n    req.language = 'en-US';\n    return next();\n  }\n  req.language = acceptLanguageHeader;\n  return next();\n};\n"
  },
  {
    "path": "src/middleware/validation/auth/index.ts",
    "content": "export * from './validatorChangePassword';\nexport * from './validatorLogin';\nexport * from './validatorRegister';\n"
  },
  {
    "path": "src/middleware/validation/auth/validatorChangePassword.ts",
    "content": "import { Request, Response, NextFunction } from 'express';\nimport validator from 'validator';\n\nimport { ConstsUser } from 'consts/ConstsUser';\nimport { CustomError } from 'utils/response/custom-error/CustomError';\nimport { ErrorValidation } from 'utils/response/custom-error/types';\n\nexport const validatorChangePassword = (req: Request, res: Response, next: NextFunction) => {\n  let { password, passwordNew, passwordConfirm } = req.body;\n  const errorsValidation: ErrorValidation[] = [];\n\n  password = !password ? '' : password;\n  passwordNew = !passwordNew ? '' : passwordNew;\n  passwordConfirm = !passwordConfirm ? '' : passwordConfirm;\n\n  if (validator.isEmpty(password)) {\n    errorsValidation.push({ password: 'Password is required' });\n  }\n\n  if (validator.isEmpty(passwordNew)) {\n    errorsValidation.push({ passwordNew: 'New password is required' });\n  }\n\n  if (validator.isEmpty(passwordConfirm)) {\n    errorsValidation.push({ passwordConfirm: 'Password confirm is required' });\n  }\n\n  if (!validator.isLength(passwordNew, { min: ConstsUser.PASSWORD_MIN_CHAR })) {\n    errorsValidation.push({\n      passwordNew: `Password must be at least ${ConstsUser.PASSWORD_MIN_CHAR} characters`,\n    });\n  }\n\n  if (!validator.equals(passwordNew, passwordConfirm)) {\n    errorsValidation.push({ passwordConfirm: 'Passwords must match' });\n  }\n\n  if (errorsValidation.length !== 0) {\n    const customError = new CustomError(\n      400,\n      'Validation',\n      'Change password validation error',\n      null,\n      null,\n      errorsValidation,\n    );\n    return next(customError);\n  }\n  return next();\n};\n"
  },
  {
    "path": "src/middleware/validation/auth/validatorLogin.ts",
    "content": "import { Request, Response, NextFunction } from 'express';\nimport validator from 'validator';\n\nimport { CustomError } from 'utils/response/custom-error/CustomError';\nimport { ErrorValidation } from 'utils/response/custom-error/types';\n\nexport const validatorLogin = (req: Request, res: Response, next: NextFunction) => {\n  let { email, password } = req.body;\n  const errorsValidation: ErrorValidation[] = [];\n\n  email = !email ? '' : email;\n  password = !password ? '' : password;\n\n  if (!validator.isEmail(email)) {\n    errorsValidation.push({ email: 'Email is invalid' });\n  }\n\n  if (validator.isEmpty(email)) {\n    errorsValidation.push({ email: 'Email field is required' });\n  }\n\n  if (validator.isEmpty(password)) {\n    errorsValidation.push({ password: 'Password field is required' });\n  }\n\n  if (errorsValidation.length !== 0) {\n    const customError = new CustomError(400, 'Validation', 'Login validation error', null, null, errorsValidation);\n    return next(customError);\n  }\n  return next();\n};\n"
  },
  {
    "path": "src/middleware/validation/auth/validatorRegister.ts",
    "content": "import { Request, Response, NextFunction } from 'express';\nimport validator from 'validator';\n\nimport { ConstsUser } from 'consts/ConstsUser';\nimport { CustomError } from 'utils/response/custom-error/CustomError';\nimport { ErrorValidation } from 'utils/response/custom-error/types';\n\nexport const validatorRegister = (req: Request, res: Response, next: NextFunction) => {\n  let { email, password, passwordConfirm } = req.body;\n  const errorsValidation: ErrorValidation[] = [];\n\n  email = !email ? '' : email;\n  password = !password ? '' : password;\n  passwordConfirm = !passwordConfirm ? '' : passwordConfirm;\n\n  if (!validator.isEmail(email)) {\n    errorsValidation.push({ email: 'Email is invalid' });\n  }\n\n  if (validator.isEmpty(email)) {\n    errorsValidation.push({ email: 'Email is required' });\n  }\n\n  if (validator.isEmpty(password)) {\n    errorsValidation.push({ password: 'Password is required' });\n  }\n\n  if (!validator.isLength(password, { min: ConstsUser.PASSWORD_MIN_CHAR })) {\n    errorsValidation.push({\n      password: `Password must be at least ${ConstsUser.PASSWORD_MIN_CHAR} characters`,\n    });\n  }\n\n  if (validator.isEmpty(passwordConfirm)) {\n    errorsValidation.push({ passwordConfirm: 'Confirm password is required' });\n  }\n\n  if (!validator.equals(password, passwordConfirm)) {\n    errorsValidation.push({ passwordConfirm: 'Passwords must match' });\n  }\n\n  if (errorsValidation.length !== 0) {\n    const customError = new CustomError(400, 'Validation', 'Register validation error', null, null, errorsValidation);\n    return next(customError);\n  }\n  return next();\n};\n"
  },
  {
    "path": "src/middleware/validation/users/index.ts",
    "content": "export * from './validatorEdit';\n"
  },
  {
    "path": "src/middleware/validation/users/validatorEdit.ts",
    "content": "import { Request, Response, NextFunction } from 'express';\nimport { getRepository } from 'typeorm';\n\nimport { User } from 'orm/entities/users/User';\nimport { CustomError } from 'utils/response/custom-error/CustomError';\nimport { ErrorValidation } from 'utils/response/custom-error/types';\n\nexport const validatorEdit = async (req: Request, res: Response, next: NextFunction) => {\n  let { username, name } = req.body;\n  const errorsValidation: ErrorValidation[] = [];\n  const userRepository = getRepository(User);\n\n  username = !username ? '' : username;\n  name = !name ? '' : name;\n\n  const user = await userRepository.findOne({ username });\n  if (user) {\n    errorsValidation.push({ username: `Username '${username}' already exists` });\n  }\n\n  if (errorsValidation.length !== 0) {\n    const customError = new CustomError(400, 'Validation', 'Edit user validation error', null, null, errorsValidation);\n    return next(customError);\n  }\n  return next();\n};\n"
  },
  {
    "path": "src/orm/config/ormconfig-seed.ts",
    "content": "import { ConnectionOptions } from 'typeorm';\nimport { SnakeNamingStrategy } from 'typeorm-naming-strategies';\n\nconst configSeed: ConnectionOptions = {\n  type: 'postgres',\n  host: process.env.PG_HOST,\n  port: Number(process.env.PG_PORT),\n  username: process.env.POSTGRES_USER,\n  password: process.env.POSTGRES_PASSWORD,\n  database: process.env.POSTGRES_DB,\n  synchronize: false,\n  logging: false,\n  entities: ['src/orm/entities/**/*.ts'],\n  migrations: ['src/orm/seeds/**/*.ts'],\n  cli: {\n    migrationsDir: 'src/orm/seeds',\n  },\n  namingStrategy: new SnakeNamingStrategy(),\n};\n\nexport = configSeed;\n"
  },
  {
    "path": "src/orm/config/ormconfig.ts",
    "content": "import { ConnectionOptions } from 'typeorm';\nimport { SnakeNamingStrategy } from 'typeorm-naming-strategies';\n\nconst config: ConnectionOptions = {\n  type: 'postgres',\n  name: 'default',\n  host: process.env.PG_HOST,\n  port: Number(process.env.PG_PORT),\n  username: process.env.POSTGRES_USER,\n  password: process.env.POSTGRES_PASSWORD,\n  database: process.env.POSTGRES_DB,\n  synchronize: false,\n  logging: false,\n  entities: ['src/orm/entities/**/*.ts'],\n  migrations: ['src/orm/migrations/**/*.ts'],\n  subscribers: ['src/orm/subscriber/**/*.ts'],\n  cli: {\n    entitiesDir: 'src/orm/entities',\n    migrationsDir: 'src/orm/migrations',\n    subscribersDir: 'src/orm/subscriber',\n  },\n  namingStrategy: new SnakeNamingStrategy(),\n};\n\nexport = config;\n"
  },
  {
    "path": "src/orm/dbCreateConnection.ts",
    "content": "import { Connection, createConnection, getConnectionManager } from 'typeorm';\n\nimport config from './config/ormconfig';\n\nexport const dbCreateConnection = async (): Promise<Connection | null> => {\n  try {\n    const conn = await createConnection(config);\n    console.log(`Database connection success. Connection name: '${conn.name}' Database: '${conn.options.database}'`);\n  } catch (err) {\n    if (err.name === 'AlreadyHasActiveConnectionError') {\n      const activeConnection = getConnectionManager().get(config.name);\n      return activeConnection;\n    }\n    console.log(err);\n  }\n  return null;\n};\n"
  },
  {
    "path": "src/orm/entities/users/User.ts",
    "content": "import bcrypt from 'bcryptjs';\nimport { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } from 'typeorm';\n\nimport { Role, Language } from './types';\n\n@Entity('users')\nexport class User {\n  @PrimaryGeneratedColumn()\n  id: number;\n\n  @Column({\n    unique: true,\n  })\n  email: string;\n\n  @Column()\n  password: string;\n\n  @Column({\n    nullable: true,\n    unique: true,\n  })\n  username: string;\n\n  @Column({\n    nullable: true,\n  })\n  name: string;\n\n  @Column({\n    default: 'STANDARD' as Role,\n    length: 30,\n  })\n  role: string;\n\n  @Column({\n    default: 'en-US' as Language,\n    length: 15,\n  })\n  language: string;\n\n  @Column()\n  @CreateDateColumn()\n  created_at: Date;\n\n  @Column()\n  @UpdateDateColumn()\n  updated_at: Date;\n\n  setLanguage(language: Language) {\n    this.language = language;\n  }\n\n  hashPassword() {\n    this.password = bcrypt.hashSync(this.password, 8);\n  }\n\n  checkIfPasswordMatch(unencryptedPassword: string) {\n    return bcrypt.compareSync(unencryptedPassword, this.password);\n  }\n}\n"
  },
  {
    "path": "src/orm/entities/users/types.ts",
    "content": "export type Role = 'ADMINISTRATOR' | 'STANDARD';\nexport type Language = 'en-US' | 'sl-SI';\n"
  },
  {
    "path": "src/orm/migrations/1590521920166-CreateUsers.ts",
    "content": "import { MigrationInterface, QueryRunner } from 'typeorm';\n\nexport class CreateUsers1590521920166 implements MigrationInterface {\n  name = 'CreateUsers1590521920166';\n\n  public async up(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(\n      `CREATE TABLE \"users\" (\"id\" SERIAL NOT NULL, \"username\" character varying(40), \"name\" character varying(40), \"email\" character varying(100) NOT NULL, \"password\" character varying NOT NULL, \"role\" character varying(30) NOT NULL DEFAULT 'STANDARD', \"language\" character varying(15) NOT NULL DEFAULT 'en-US', \"created_at\" TIMESTAMP NOT NULL DEFAULT now(), \"updated_at\" TIMESTAMP NOT NULL DEFAULT now(), CONSTRAINT \"UQ_fe0bb3f6520ee0469504521e710\" UNIQUE (\"username\"), CONSTRAINT \"UQ_97672ac88f789774dd47f7c8be3\" UNIQUE (\"email\"), CONSTRAINT \"PK_a3ffb1c0c8416b9fc6f907b7433\" PRIMARY KEY (\"id\"))`,\n      undefined,\n    );\n  }\n\n  public async down(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(`DROP TABLE \"users\"`, undefined);\n  }\n}\n"
  },
  {
    "path": "src/orm/seeds/1590519635401-SeedUsers.ts",
    "content": "import { MigrationInterface, QueryRunner, getRepository } from 'typeorm';\n\nimport { Role } from '../entities/users/types';\nimport { User } from '../entities/users/User';\n\nexport class SeedUsers1590519635401 implements MigrationInterface {\n  public async up(queryRunner: QueryRunner): Promise<any> {\n    let user = new User();\n    const userRepository = getRepository(User);\n\n    user.username = 'Heisenberg';\n    user.name = 'Walter White';\n    user.email = 'admin@admin.com';\n    user.password = 'pass1';\n    user.hashPassword();\n    user.role = 'ADMINISTRATOR' as Role;\n    await userRepository.save(user);\n\n    user = new User();\n    user.username = 'Jesse';\n    user.name = 'Jesse Pinkman';\n    user.email = 'standard@standard.com';\n    user.password = 'pass1';\n    user.hashPassword();\n    user.role = 'STANDARD' as Role;\n    await userRepository.save(user);\n\n    user = new User();\n    user.username = 'Sky';\n    user.name = 'Skyler White';\n    user.email = 'skyler.white@test.com';\n    user.password = 'pass1';\n    user.hashPassword();\n    await userRepository.save(user);\n\n    user = new User();\n    user.username = 'Hank';\n    user.name = 'Hank Schrader';\n    user.email = 'hank.schrader@test.com';\n    user.password = 'pass1';\n    user.hashPassword();\n    await userRepository.save(user);\n\n    user = new User();\n    user.username = 'Marie';\n    user.name = 'Marie Schrader';\n    user.email = 'marie.schrader@test.com';\n    user.password = 'pass1';\n    user.hashPassword();\n    await userRepository.save(user);\n\n    user = new User();\n    user.username = 'The Lawyer';\n    user.name = 'Saul Goodman';\n    user.email = 'saul.goodman@test.com';\n    user.password = 'pass1';\n    user.hashPassword();\n    await userRepository.save(user);\n\n    user = new User();\n    user.username = 'Gus';\n    user.name = 'Gustavo Fring';\n    user.email = 'gustavo.fring@test.com';\n    user.password = 'pass1';\n    user.hashPassword();\n    await userRepository.save(user);\n\n    user = new User();\n    user.username = 'Mike';\n    user.name = 'Michael Ehrmantraut';\n    user.email = 'michael.ehrmantraut@test.com';\n    user.password = 'pass1';\n    user.hashPassword();\n    await userRepository.save(user);\n\n    user = new User();\n    user.username = 'Tio';\n    user.name = 'Hector Salamanca';\n    user.email = 'hector.salamanca@test.com';\n    user.password = 'pass1';\n    user.hashPassword();\n    await userRepository.save(user);\n\n    user = new User();\n    user.username = 'Tuco';\n    user.name = 'Alberto Salamanca';\n    user.email = 'alberto.salamanca@test.com';\n    user.password = 'pass1';\n    user.hashPassword();\n    await userRepository.save(user);\n  }\n\n  public async down(queryRunner: QueryRunner): Promise<any> {\n    console.log('Not implemented');\n  }\n}\n"
  },
  {
    "path": "src/routes/index.ts",
    "content": "import { Router } from 'express';\n\nimport page404 from './pages/404';\nimport pageRoot from './pages/root';\nimport v1 from './v1/';\n\nconst router = Router();\n\nrouter.use(`/v1`, v1);\n\nrouter.use(pageRoot);\nrouter.use(page404);\n\nexport default router;\n"
  },
  {
    "path": "src/routes/pages/404.ts",
    "content": "import { Router } from 'express';\n\nconst router = Router();\n\nrouter.get('*', (req, res, next) => {\n  return res.status(404).json('404 Not Found');\n});\n\nexport default router;\n"
  },
  {
    "path": "src/routes/pages/root.ts",
    "content": "import { Router } from 'express';\n\nconst router = Router();\n\nrouter.get('/', (req, res, next) => {\n  res.status(200).header('Content-Type', 'text/html').send(`<h4>💊 RESTful API boilerplate</h4>`);\n});\n\nexport default router;\n"
  },
  {
    "path": "src/routes/v1/auth.ts",
    "content": "import { Router } from 'express';\n\nimport { login, register, changePassword } from 'controllers/auth';\nimport { checkJwt } from 'middleware/checkJwt';\nimport { validatorLogin, validatorRegister, validatorChangePassword } from 'middleware/validation/auth';\n\nconst router = Router();\n\nrouter.post('/login', [validatorLogin], login);\nrouter.post('/register', [validatorRegister], register);\nrouter.post('/change-password', [checkJwt, validatorChangePassword], changePassword);\n\nexport default router;\n"
  },
  {
    "path": "src/routes/v1/index.ts",
    "content": "import { Router } from 'express';\n\nimport auth from './auth';\nimport users from './users';\n\nconst router = Router();\n\nrouter.use('/auth', auth);\nrouter.use('/users', users);\n\nexport default router;\n"
  },
  {
    "path": "src/routes/v1/users.ts",
    "content": "import { Router } from 'express';\n\nimport { list, show, edit, destroy } from 'controllers/users';\nimport { checkJwt } from 'middleware/checkJwt';\nimport { checkRole } from 'middleware/checkRole';\nimport { validatorEdit } from 'middleware/validation/users';\n\nconst router = Router();\n\nrouter.get('/', [checkJwt, checkRole(['ADMINISTRATOR'])], list);\n\nrouter.get('/:id([0-9]+)', [checkJwt, checkRole(['ADMINISTRATOR'], true)], show);\n\nrouter.patch('/:id([0-9]+)', [checkJwt, checkRole(['ADMINISTRATOR'], true), validatorEdit], edit);\n\nrouter.delete('/:id([0-9]+)', [checkJwt, checkRole(['ADMINISTRATOR'], true)], destroy);\n\nexport default router;\n"
  },
  {
    "path": "src/types/JwtPayload.ts",
    "content": "import { Role } from '../orm/entities/users/types';\n\nexport type JwtPayload = {\n  id: number;\n  name: string;\n  email: string;\n  role: Role;\n  created_at: Date;\n};\n"
  },
  {
    "path": "src/types/ProcessEnv.d.ts",
    "content": "declare namespace NodeJS {\n  export interface ProcessEnv {\n    PORT: string;\n    NODE_ENV: string;\n    PG_HOST: string;\n    PG_PORT: string;\n    POSTGRES_USER: string;\n    POSTGRES_PASSWORD: string;\n    POSTGRES_DB: string;\n    JWT_SECRET: string;\n    JWT_EXPIRATION: string;\n  }\n}\n"
  },
  {
    "path": "src/types/express/index.d.ts",
    "content": "import { Language } from 'orm/entities/users/types';\n\nimport { JwtPayload } from '../JwtPayload';\n\ndeclare global {\n  namespace Express {\n    export interface Request {\n      jwtPayload: JwtPayload;\n      language: Language;\n    }\n    export interface Response {\n      customSuccess(httpStatusCode: number, message: string, data?: any): Response;\n    }\n  }\n}\n"
  },
  {
    "path": "src/utils/createJwtToken.ts",
    "content": "import jwt from 'jsonwebtoken';\n\nimport { JwtPayload } from '../types/JwtPayload';\n\nexport const createJwtToken = (payload: JwtPayload): string => {\n  return jwt.sign(payload, process.env.JWT_SECRET!, {\n    expiresIn: process.env.JWT_EXPIRATION,\n  });\n};\n"
  },
  {
    "path": "src/utils/response/custom-error/CustomError.ts",
    "content": "import { ErrorType, ErrorValidation, ErrorResponse } from './types';\n\nexport class CustomError extends Error {\n  private httpStatusCode: number;\n  private errorType: ErrorType;\n  private errors: string[] | null;\n  private errorRaw: any;\n  private errorsValidation: ErrorValidation[] | null;\n\n  constructor(\n    httpStatusCode: number,\n    errorType: ErrorType,\n    message: string,\n    errors: string[] | null = null,\n    errorRaw: any = null,\n    errorsValidation: ErrorValidation[] | null = null,\n  ) {\n    super(message);\n\n    this.name = this.constructor.name;\n\n    this.httpStatusCode = httpStatusCode;\n    this.errorType = errorType;\n    this.errors = errors;\n    this.errorRaw = errorRaw;\n    this.errorsValidation = errorsValidation;\n  }\n\n  get HttpStatusCode() {\n    return this.httpStatusCode;\n  }\n\n  get JSON(): ErrorResponse {\n    return {\n      errorType: this.errorType,\n      errorMessage: this.message,\n      errors: this.errors,\n      errorRaw: this.errorRaw,\n      errorsValidation: this.errorsValidation,\n      stack: this.stack,\n    };\n  }\n}\n"
  },
  {
    "path": "src/utils/response/custom-error/types.ts",
    "content": "export type ErrorResponse = {\n  errorType: ErrorType;\n  errorMessage: string;\n  errors: string[] | null;\n  errorRaw: any;\n  errorsValidation: ErrorValidation[] | null;\n  stack?: string;\n};\n\nexport type ErrorType = 'General' | 'Raw' | 'Validation' | 'Unauthorized';\n\nexport type ErrorValidation = { [key: string]: string };\n"
  },
  {
    "path": "src/utils/response/customSuccess.ts",
    "content": "import { response, Response } from 'express';\n\nresponse.customSuccess = function (httpStatusCode: number, message: string, data: any = null): Response {\n  return this.status(httpStatusCode).json({ message, data });\n};\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"es6\",\n    \"module\": \"commonjs\",\n    \"lib\": [\"dom\", \"es6\", \"es2017\", \"esnext.asynciterable\"],\n    \"sourceMap\": true,\n    \"outDir\": \"./dist\",\n    \"moduleResolution\": \"node\",\n    \"removeComments\": true,\n    \"allowSyntheticDefaultImports\": true,\n    \"esModuleInterop\": true,\n    \"emitDecoratorMetadata\": true,\n    \"experimentalDecorators\": true,\n    \"resolveJsonModule\": true,\n    \"typeRoots\": [\"./src/types\", \"./node_modules/@types\"],\n    \"baseUrl\": \"src/\",\n    \"noImplicitReturns\": true,\n    \"noFallthroughCasesInSwitch\": true\n  },\n  \"include\": [\"./src/**/*.tsx\", \"./src/**/*.ts\"],\n  \"exclude\": [\"node_modules\", \"test/**/*.ts\"]\n}\n"
  }
]