├── .gitignore ├── Dockerfile.development ├── Dockerfile.production ├── Dockerfile.staging ├── Readme.md ├── app.js ├── docker-compose.yml ├── install.sh ├── jsconfig.json ├── package.json ├── src ├── bin │ ├── cli │ └── www ├── config │ ├── cli.env │ ├── development.env │ ├── index.js │ ├── staging.env │ └── test.env ├── console │ ├── commands │ │ └── PrintArgs.js │ └── kernel.js ├── containers │ ├── cli.js │ ├── test.js │ └── web.js ├── controllers │ ├── api │ │ ├── AuthenticationAPIController │ │ │ ├── __test__ │ │ │ │ ├── currentUser.test.js │ │ │ │ ├── index.test.js │ │ │ │ ├── login.test.js │ │ │ │ ├── signup.test.js │ │ │ │ └── updatePassword.test.js │ │ │ └── index.js │ │ └── MainAPIController │ │ │ └── index.js │ └── web │ │ └── .gitkeep ├── events │ ├── EventBase.js │ ├── UserLoggedInEvent.js │ └── UserUpdatedEvent.js ├── general │ └── .gitkeep ├── helpers │ ├── ArgHelper │ │ └── index.js │ ├── DbAdapter │ │ └── index.js │ ├── EnvHelper │ │ └── index.js │ ├── EventHelper │ │ └── index.js │ ├── PrintHelper │ │ └── index.js │ ├── SecurityHelper │ │ └── index.js │ ├── Serializer │ │ └── index.js │ ├── SocketAdapter │ │ └── index.js │ ├── StringHelper │ │ └── index.js │ └── TestHelper │ │ └── index.js ├── listeners │ ├── ListenerBase.js │ ├── UserLoggedInEventListener.js │ └── UserUpdatedEventListener.js ├── middlewares │ ├── AuthMiddleware.js │ ├── ContentTypeHandler.js │ ├── CorsMiddleware.js │ ├── ErrorResponseMiddleware.js │ └── HttpsMiddleware.js ├── models │ ├── .gitkeep │ ├── Business │ │ ├── .gitkeep │ │ └── Exeption │ │ │ ├── HttpNotAcceptableException.js │ │ │ ├── InterfaceException.js │ │ │ └── UserDefinedException.js │ └── Domain │ │ ├── .gitkeep │ │ ├── ModelBase.js │ │ └── User │ │ ├── index.js │ │ └── schema.js ├── repositories │ ├── .gitkeep │ ├── Interfaces │ │ └── IUserRepository.js │ └── Vendor │ │ ├── MongoDb │ │ └── UserRepository │ │ │ ├── __test__ │ │ │ ├── create.test.js │ │ │ └── index.test.js │ │ │ └── index.js │ │ └── Test │ │ └── UserRepository │ │ ├── index.js │ │ └── seed.js ├── routes │ ├── api │ │ └── index.js │ ├── console │ │ └── index.js │ ├── event │ │ └── index.js │ ├── index.js │ ├── jobs │ │ └── index.js │ ├── socket │ │ └── index.js │ └── web │ │ └── index.js ├── services │ ├── .gitkeep │ ├── LoggerService │ │ └── index.js │ └── UserService │ │ ├── __test__ │ │ ├── getUser.test.js │ │ └── index.test.js │ │ └── index.js ├── tests │ └── index.test.js └── views │ └── .gitkeep ├── start.sh ├── stop.sh └── uninstall.sh /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/node 3 | 4 | ### Node ### 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | 24 | # nyc test coverage 25 | .nyc_output 26 | 27 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 28 | .grunt 29 | 30 | # Bower dependency directory (https://bower.io/) 31 | bower_components 32 | 33 | # node-waf configuration 34 | .lock-wscript 35 | 36 | # Compiled binary addons (https://nodejs.org/api/addons.html) 37 | build/Release 38 | 39 | # Dependency directories 40 | node_modules/ 41 | jspm_packages/ 42 | 43 | # TypeScript v1 declaration files 44 | typings/ 45 | 46 | # Optional npm cache directory 47 | .npm 48 | 49 | # Optional eslint cache 50 | .eslintcache 51 | 52 | # Optional REPL history 53 | .node_repl_history 54 | 55 | # Output of 'npm pack' 56 | *.tgz 57 | 58 | # Yarn Integrity file 59 | .yarn-integrity 60 | 61 | # dotenv environment variables file 62 | # .env 63 | 64 | package-lock.json 65 | 66 | # parcel-bundler cache (https://parceljs.org/) 67 | .cache 68 | 69 | # next.js build output 70 | .next 71 | 72 | # nuxt.js build output 73 | .nuxt 74 | 75 | # vuepress build output 76 | .vuepress/dist 77 | 78 | # Serverless directories 79 | .serverless 80 | 81 | 82 | # End of https://www.gitignore.io/api/node 83 | 84 | production.env 85 | docs 86 | Dockerfile 87 | src/config/certs/**/* 88 | -------------------------------------------------------------------------------- /Dockerfile.development: -------------------------------------------------------------------------------- 1 | FROM node:8.9.1 2 | 3 | WORKDIR /var/www/app 4 | 5 | COPY ./src /var/www/app/src 6 | COPY ./package.json /var/www/app/package.json 7 | COPY ./app.js /var/www/app/app.js 8 | 9 | RUN npm install -g nodemon pm2 10 | RUN npm install 11 | 12 | CMD ["npm", "run", "start:dev"] -------------------------------------------------------------------------------- /Dockerfile.production: -------------------------------------------------------------------------------- 1 | FROM node:8.9.1 2 | 3 | WORKDIR /var/www/app 4 | 5 | COPY ./src /var/www/app/src 6 | COPY ./package.json /var/www/app/package.json 7 | COPY ./app.js /var/www/app/app.js 8 | 9 | RUN npm install -g nodemon pm2 10 | RUN npm install 11 | 12 | CMD ["npm", "run", "start:prod"] -------------------------------------------------------------------------------- /Dockerfile.staging: -------------------------------------------------------------------------------- 1 | FROM node:8.9.1 2 | 3 | WORKDIR /var/www/app 4 | 5 | COPY ./src /var/www/app/src 6 | COPY ./package.json /var/www/app/package.json 7 | COPY ./app.js /var/www/app/app.js 8 | 9 | RUN npm install -g nodemon pm2 10 | RUN npm install 11 | 12 | CMD ["npm", "run", "start:staging"] -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # Node App - Server 2 | 3 | This repository contains the Node App developed using Node JS and other server side technologies. 4 | 5 | 6 | ## Developers 7 | * Vigan Abdurrahmani - 27/02/2018 8 | 9 | 10 | ## Technologies ## 11 | * Node JS: [https://nodejs.org/](https://nodejs.org/) 12 | * Docker: [https://www.docker.com/](https://www.docker.com/) 13 | * Mongo DB: [https://mongodb.com/](https://mongodb.com/) 14 | 15 | 16 | ## Setup 17 | 18 | ### Prequises 19 | In order to run the project you must have configured the following softwares/services in your hosting environment: 20 | * Docker - [https://www.digitalocean.com/community/tutorials/how-to-install-and-use-docker-on-ubuntu-16-04](https://www.digitalocean.com/community/tutorials/how-to-install-and-use-docker-on-ubuntu-16-04) 21 | * Docker-Compose - [https://www.digitalocean.com/community/tutorials/how-to-install-docker-compose-on-ubuntu-16-04](https://www.digitalocean.com/community/tutorials/how-to-install-docker-compose-on-ubuntu-16-04) 22 | 23 | If you prefer to manage all the required technologies without docker then you have to install the following services: 24 | * Node JS v8.9.2 - [https://www.digitalocean.com/community/tutorials/how-to-install-node-js-on-ubuntu-16-04](https://www.digitalocean.com/community/tutorials/how-to-install-node-js-on-ubuntu-16-04) 25 | * Mongo DB - [https://www.digitalocean.com/community/tutorials/how-to-install-mongodb-on-ubuntu-16-04](https://www.digitalocean.com/community/tutorials/how-to-install-mongodb-on-ubuntu-16-04) 26 | 27 | 28 | ### Server Configuration via Docker 29 | 30 | **Before setting up the project make sure you have installed the following packages:** 31 | * Docker CE, 32 | * Docker Compose 33 | 34 | The project can be setup on live environment just by using the following command: 35 | ```terminal 36 | ./install.sh 37 | ``` 38 | 39 | In case if you want to run the project on development environment just use the following command: 40 | ```terminal 41 | ./install.sh --dev 42 | ``` 43 | 44 | To remove the project completely from the environment use: 45 | ```terminal 46 | ./uninstall.sh 47 | ``` 48 | 49 | To stop the docker container (not delete) use: 50 | ```terminal 51 | ./stop.sh 52 | ``` 53 | 54 | To start the docker container (in case if it's stopped) use: 55 | ```terminal 56 | ./start.sh 57 | ``` 58 | 59 | ### Server Configuration without Docker 60 | First you have to install the node modules in root directory via: 61 | ```terminal 62 | npm install 63 | ``` 64 | 65 | After that you can start the server in production or development mode via `npm run start:dev` or npm `run start:prod` 66 | 67 | In case if you're using nginx here's a simple configuration file that may help you to setup the system: 68 | ```nginx 69 | server { 70 | listen 80; 71 | listen [::]:80; 72 | 73 | server_name example.com; 74 | 75 | location / { 76 | proxy_pass http://127.0.0.1:4000; 77 | } 78 | 79 | location /api { 80 | proxy_pass http://127.0.0.1:4001; 81 | } 82 | 83 | proxy_set_header X-Real-IP $remote_addr; 84 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 85 | proxy_set_header X-Forwarded-Proto https; 86 | proxy_set_header X-Forwarded-Port 443; 87 | proxy_set_header Host $host; 88 | } 89 | ``` 90 | 91 | 92 | ## API Documentation 93 | In order to generate the full API documentation for the system you just have to run the following command: 94 | ```terminal 95 | npm run doc 96 | ``` 97 | 98 | In case if you've installed the system via Docker then you must first access the container via bash with the following command: 99 | ```terminal 100 | docker exec -it node_app_server bash 101 | ``` 102 | 103 | ## Running Test 104 | In order to run test you just have to run the following command: 105 | ```terminal 106 | npm run test 107 | ``` 108 | 109 | ## Running CLI Env 110 | In order to run cli commands supported from your app run the following command: 111 | ```terminal 112 | npm run cli -- 113 | ``` 114 | In staging or production run: 115 | ```terminal 116 | npm run cli: -- 117 | 118 | If you're using Docker don't forget to access the container first as mentioned in "API Documentation" section! -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const createError = require('http-errors'); 3 | const bodyParser = require('body-parser'); 4 | const cookieParser = require('cookie-parser'); 5 | const HttpsMiddleware = require('@middlewares/HttpsMiddleware'); 6 | const CorsMiddleware = require('@middlewares/CorsMiddleware'); 7 | 8 | 9 | // APP SETUP 10 | const app = express(); 11 | const config = require('@config'); 12 | const container = require('@containers/web'); 13 | const routes = require('@routes'); 14 | 15 | // SECURITY CONFIG 16 | app.set('trust proxy', 1); 17 | app.use(CorsMiddleware); 18 | 19 | // REQUEST CONFIG 20 | app.set('json spaces', 2); 21 | app.use(cookieParser()); 22 | app.use(bodyParser.json({type: 'application/json', limit: config.REQ_PAYLOAD_LIMIT})); 23 | app.use(express.json()); 24 | app.use(express.urlencoded({ extended: false })); 25 | 26 | if(config.APP_ENV === 'production') { 27 | app.use(HttpsMiddleware); 28 | } 29 | 30 | // ROUTE CONFIG 31 | app.use(routes.web(container)); 32 | app.use(routes.api(container)); 33 | 34 | // EVENT CONFIG 35 | routes.event(container); 36 | 37 | // JOBS CONFIG 38 | routes.jobs(container); 39 | 40 | // SOCKET CONFIG 41 | routes.socket(container); 42 | 43 | // 404 ERROR HANDLER 44 | app.use((req, res, next) => { 45 | next(createError(404)); 46 | }); 47 | 48 | // ERROR HANDLER 49 | // This middleware is injected vi Awilix because in future it may adapt to different environments 50 | app.use(container.resolve('ErrorResponseMiddleware').handler); 51 | 52 | app.container = container; 53 | 54 | app.on('close', () => container.dispose()); 55 | module.exports = app; -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.3' 2 | 3 | services: 4 | node_app_db: 5 | image: mongo:4.0 6 | container_name: node_app_db 7 | environment: 8 | MONGO_INITDB_ROOT_USERNAME: root 9 | MONGO_INITDB_ROOT_PASSWORD: root 10 | MONGO_INITDB_DATABASE: node_app 11 | ports: 12 | - 37017:27017 13 | - 37018:27018 14 | 15 | node_app_server: 16 | build: . 17 | image: node_app_server:1.0 18 | container_name: node_app_server 19 | working_dir: /var/www/app 20 | depends_on: 21 | - node_app_db 22 | volumes: 23 | - ./src:/var/www/app/src 24 | - ./docs:/var/www/app/docs 25 | - ./package.json:/var/www/app/package.json 26 | - ./app.js:/var/www/app/app.js 27 | - ./app.log:/var/www/app/app.log 28 | ports: 29 | - 4001:4001 30 | - 4002:4002 31 | environment: 32 | - NODE_ENV=development 33 | - PUBLIC_URL=/ -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | APP_CONTAINER="node_app_server" 3 | DB_CONTAINER="node_app_db" 4 | DB_ADMIN_USR="root" 5 | DB_ADMIN_PWD="root" 6 | DB_ADMIN_DBNAME="admin" 7 | DB_WWW_USR="www" 8 | DB_WWW_PWD="www" 9 | DB_WWW_DBNAME="node_app" 10 | DB_TEST_NAME="node_app_test" 11 | 12 | DOCKER_CMD="docker" 13 | DOCKER_COMPOSE_CMD="docker-compose" 14 | 15 | 16 | if [ ! -f "app.log" ]; then 17 | touch app.log 18 | fi 19 | 20 | 21 | if [ "$1" == "--dev" ]; then 22 | cp Dockerfile.development Dockerfile 23 | elif [ "$1" == '--staging' ]; then 24 | cp Dockerfile.staging Dockerfile 25 | else 26 | cp Dockerfile.production Dockerfile 27 | fi 28 | 29 | 30 | echo "Installing Node App - Server >>>" 31 | 32 | ## Boot db microservice 33 | $DOCKER_COMPOSE_CMD up -d $DB_CONTAINER; 34 | 35 | ## Build node microservice 36 | $DOCKER_COMPOSE_CMD build $APP_CONTAINER; 37 | 38 | ## Setup client database on db microservice 39 | $DOCKER_CMD exec -it $DB_CONTAINER mongo $DB_ADMIN_DBNAME -u $DB_ADMIN_USR -p $DB_ADMIN_PWD --eval "db.createUser({ \ 40 | user: '$DB_WWW_USR',\ 41 | pwd: '$DB_WWW_PWD',\ 42 | roles: [\ 43 | {role: 'readWrite', db: '$DB_WWW_DBNAME'},\ 44 | {role: 'readWrite', db: '$DB_TEST_NAME'}\ 45 | ]\ 46 | });" 47 | $DOCKER_CMD exec -it $DB_CONTAINER mongo $DB_WWW_DBNAME -u $DB_WWW_USR -p $DB_WWW_PWD --authenticationDatabase $DB_ADMIN_DBNAME --eval "db.bootLogs.insert({ message: '$DB_WWW_DBNAME database created on ' + new Date() });" 48 | $DOCKER_CMD exec -it $DB_CONTAINER mongo $DB_WWW_DBNAME -u $DB_WWW_USR -p $DB_WWW_PWD --authenticationDatabase $DB_ADMIN_DBNAME --eval "db.bootLogs.isCapped();" 49 | 50 | $DOCKER_CMD exec -it $DB_CONTAINER mongo $DB_TEST_NAME -u $DB_WWW_USR -p $DB_WWW_PWD --authenticationDatabase $DB_ADMIN_DBNAME --eval "db.bootLogs.insert({ message: '$DB_TEST_NAME database created on ' + new Date() })"; 51 | $DOCKER_CMD exec -it $DB_CONTAINER mongo $DB_TEST_NAME -u $DB_WWW_USR -p $DB_WWW_PWD --authenticationDatabase $DB_ADMIN_DBNAME --eval "db.bootLogs.isCapped();" 52 | 53 | ## Boot node microservice 54 | if [ "$1" == "--dev" ]; then 55 | $DOCKER_COMPOSE_CMD up $APP_CONTAINER; 56 | elif [ "$1" == '--staging' ]; then 57 | $DOCKER_COMPOSE_CMD up -d $APP_CONTAINER; 58 | else 59 | if [ ! -f "./src/config/production.env" ]; then 60 | cp ./src/config/staging.env ./src/config/production.env 61 | fi 62 | 63 | $DOCKER_COMPOSE_CMD up -d $DB_CONTAINER; 64 | $DOCKER_COMPOSE_CMD up -d $APP_CONTAINER; 65 | fi -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es2016", 5 | "jsx": "preserve", 6 | "baseUrl": ".", 7 | "paths": { 8 | "@config": [ 9 | "src/config/index" 10 | ], 11 | "@config/*": [ 12 | "src/config/*" 13 | ], 14 | "@console/*": [ 15 | "src/console/*" 16 | ], 17 | "@containers/*": [ 18 | "src/containers/*" 19 | ], 20 | "@controllers/*": [ 21 | "src/controllers/*" 22 | ], 23 | "@events/*": [ 24 | "src/events/*" 25 | ], 26 | "@helpers/*": [ 27 | "src/helpers/*" 28 | ], 29 | "@listeners/*": [ 30 | "src/listeners/*" 31 | ], 32 | "@middlewares/*": [ 33 | "src/middlewares/*" 34 | ], 35 | "@models/*": [ 36 | "src/models/*" 37 | ], 38 | "@routes": [ 39 | "src/routes/index" 40 | ], 41 | "@routes/*": [ 42 | "src/routes/*" 43 | ], 44 | "@services/*": [ 45 | "src/services/*" 46 | ], 47 | "@repositories/*": [ 48 | "src/repositories/*" 49 | ], 50 | } 51 | }, 52 | "include": [ 53 | "src/**/*.js" 54 | ], 55 | "exclude": [ 56 | "node_modules", 57 | "**/node_modules/*" 58 | ] 59 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-js-api-boilerplate", 3 | "author": { 4 | "name": "vigan.abd", 5 | "email": "vigan.abd@gmail.com" 6 | }, 7 | "version": "1.0.0", 8 | "private": true, 9 | "scripts": { 10 | "cli": "NODE_ENV=development node src/bin/cli", 11 | "cli:staging": "NODE_ENV=staging node src/bin/cli", 12 | "cli:prod": "NODE_ENV=production node src/bin/cli", 13 | "start": "npm run start:dev", 14 | "start:dev": "PORT=4001 NODE_ENV=development nodemon ./src/bin/www", 15 | "start:staging": "PORT=4001 NODE_ENV=staging pm2-runtime ./src/bin/www", 16 | "start:prod": "PORT=4001 NODE_ENV=production pm2-runtime ./src/bin/www", 17 | "doc": "./node_modules/.bin/apidoc -i ./src/controllers/ -o ./docs/api", 18 | "test": "NODE_ENV=test ./node_modules/.bin/mocha ./src/tests/index.test.js --timeout 5000" 19 | }, 20 | "_moduleAliases": { 21 | "@config": "src/config/", 22 | "@console": "src/console/", 23 | "@containers": "src/containers/", 24 | "@controllers": "src/controllers/", 25 | "@events": "src/events/", 26 | "@helpers": "src/helpers/", 27 | "@listeners": "src/listeners/", 28 | "@middlewares": "src/middlewares/", 29 | "@models": "src/models/", 30 | "@routes": "src/routes/", 31 | "@services": "src/services/", 32 | "@repositories": "src/repositories/" 33 | }, 34 | "dependencies": { 35 | "awilix": "^3.0.9", 36 | "bcrypt-nodejs": "0.0.3", 37 | "body-parser": "^1.18.3", 38 | "command-line-args": "^5.0.2", 39 | "cookie-parser": "~1.4.3", 40 | "debug": "~2.6.9", 41 | "ejs": "~2.5.7", 42 | "env-var": "^3.3.0", 43 | "express": "~4.16.0", 44 | "http-errors": "~1.6.2", 45 | "joi": "^13.6.0", 46 | "jwt-simple": "^0.5.1", 47 | "module-alias": "^2.2.0", 48 | "mongoose": "^5.7.5", 49 | "mongoose-paginate-v2": "^1.0.12", 50 | "node-cron": "^2.0.3", 51 | "node-env-file": "^0.1.8", 52 | "object-to-xml": "^2.0.0", 53 | "simple-express-route-builder": "^0.1.1", 54 | "socket.io": "^2.2.0", 55 | "uuid": "^3.3.2", 56 | "winston": "^2.3.0", 57 | "winston-aws-cloudwatch": "^3.0.0" 58 | }, 59 | "devDependencies": { 60 | "apidoc": "^0.17.6", 61 | "assert": "^1.4.1", 62 | "chai": "^4.2.0", 63 | "mocha": "^7.1.1", 64 | "node-mocks-http": "^1.7.3", 65 | "sinon": "^7.1.1" 66 | }, 67 | "apidoc": { 68 | "name": "Node App API", 69 | "version": "0.1.0", 70 | "description": "API Powering Node App.", 71 | "title": "Node App API Documentation", 72 | "url": "http://127.0.0.1:4001" 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/bin/cli: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | require('module-alias/register'); 7 | 8 | const kernel = require('../console/kernel'); 9 | kernel(); -------------------------------------------------------------------------------- /src/bin/www: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | require('module-alias/register'); 7 | const app = require('../../app'); 8 | const config = require('../config'); 9 | const loggerService = app.container.resolve('loggerService'); 10 | const HttpServer = require('http').Server; 11 | 12 | /** 13 | * Get port from environment and store in Express. 14 | */ 15 | app.set('port', config.PORT); 16 | 17 | /** 18 | * Create HTTP server. 19 | */ 20 | const server = HttpServer(app); 21 | 22 | /** 23 | * Listen on provided port, on all network interfaces. 24 | */ 25 | server.listen(config.PORT, config.HOST, err => { 26 | if (err) { 27 | loggerService.log('error', err, { tags: 'cron' }); 28 | } 29 | loggerService.log('info', `[STARTUP] Server listening on ${config.HOST}:${config.PORT}`, { tags: 'startup,network' }); 30 | }); -------------------------------------------------------------------------------- /src/config/cli.env: -------------------------------------------------------------------------------- 1 | APP_NAME="Node App" 2 | APP_ENV=development 3 | APP_DEBUG=true 4 | HOST=0.0.0.0 5 | PORT=4001 6 | 7 | JWT_SECRET=michaeljordan23 8 | JWT_EXPIRE_TIME=3600 9 | 10 | LOG_LEVEL=silly 11 | 12 | DB_CONNECTION=mongo 13 | DB_HOST=node_app_db 14 | DB_PORT=27017 15 | DB_DATABASE_AUTH=admin 16 | DB_DATABASE=node_app 17 | DB_USERNAME=root 18 | DB_PASSWORD=root -------------------------------------------------------------------------------- /src/config/development.env: -------------------------------------------------------------------------------- 1 | APP_NAME="Node App" 2 | APP_ENV=development 3 | APP_DEBUG=true 4 | HOST=0.0.0.0 5 | PORT=4001 6 | 7 | JWT_SECRET=michaeljordan23 8 | JWT_EXPIRE_TIME=3600 9 | 10 | LOG_LEVEL=silly 11 | LOG_GROUP_NAME=node-app 12 | CLOUDWATCH_REGION=ap-southeast-2 13 | CLOUDWATCH_ACCESS_KEY_ID= 14 | CLOUDWATCH_SECRET_ACCESS_KEY= 15 | 16 | DB_CONNECTION=mongo 17 | DB_HOST=node_app_db 18 | DB_PORT=27017 19 | DB_DATABASE_AUTH=admin 20 | DB_DATABASE=node_app 21 | DB_USERNAME=root 22 | DB_PASSWORD=root 23 | 24 | SOCKET_PORT=4002 25 | SOCKET_PATH="/io" 26 | SOCKET_SECURE=false -------------------------------------------------------------------------------- /src/config/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * For each environment we have different configuration, e.g. for console commands we can use cli.env, for testing test.env. 3 | * All of these files are resolved as environment variables via 'node-env-file' package which loads the content of files 4 | * into environment variables. All it takes is NODE_ENV environment variable to resolve the file and load entire config. 5 | * E.g. NODE_ENV=test ./node_modules/.bin/mocha ./src/tests/index.test.js loads test.env file config. We also have a helper method 6 | * env which tries to load an environment variable based on key, and provides a default value if the variable doesn't exist. E.g 7 | * env('JWT_EXPIRE_TIME', 3600) tries to load environment variable process.env.JWT_EXPIRE_TIME and provides a default value 3600 8 | * if it misses. 9 | */ 10 | 11 | const envFile = require('node-env-file'); 12 | const path = require('path'); 13 | const env = require('@helpers/EnvHelper'); 14 | 15 | try { 16 | envFile(path.join(__dirname, process.env.NODE_ENV + '.env')); 17 | } catch (e) { 18 | console.log(`No config file found for ${process.env.NODE_ENV}`); 19 | } 20 | 21 | const APP_ENV = env('APP_ENV', process.env.NODE_ENV || 'development'); 22 | const HOST = env('HOST', '0.0.0.0'); 23 | 24 | const config = { 25 | // ENV 26 | APP_NAME: env('APP_NAME', 'Node JS'), 27 | APP_ENV: APP_ENV, 28 | APP_DEBUG: env('APP_DEBUG', true), 29 | PORT: env('PORT', process.env.PORT || '4001'), 30 | HOST: HOST, 31 | ROOT: `${__dirname}/..`, 32 | 33 | // SECURITY 34 | JWT_SECRET: env('JWT_SECRET', 'notsosecure'), 35 | JWT_EXPIRE_TIME: env('JWT_EXPIRE_TIME', 3600), //seconds 36 | 37 | // REQUEST 38 | REQ_PAYLOAD_LIMIT: env('REQ_PAYLOAD_LIMIT', '50mb'), 39 | 40 | // DB 41 | DB_CONNECTION_STRING: `mongodb://${env('DB_HOST', '127.0.0.1')}:${env('DB_PORT', 27017)}/${env('DB_DATABASE', "admin")}`, 42 | 43 | // SOCKET 44 | SOCKET_PORT: env('SOCKET_PORT', 4002), 45 | SOCKET_PATH: env('SOCKET_PATH', '/io'), 46 | SOCKET_SECURE: env('SOCKET_SECURE', null) == 'true', 47 | 48 | // LOG 49 | LOG_LEVEL: env('LOG_LEVEL', 'silly'), 50 | LOG_GROUP_NAME: env('LOG_GROUP_NAME', 'node-app'), 51 | 52 | // CLOUDWATCH 53 | CLOUDWATCH_REGION: env('CLOUDWATCH_REGION', 'ap-southeast-2'), 54 | CLOUDWATCH_ACCESS_KEY_ID: env('CLOUDWATCH_ACCESS_KEY_ID', ''), 55 | CLOUDWATCH_SECRET_ACCESS_KEY: env('CLOUDWATCH_SECRET_ACCESS_KEY', ''), 56 | }; 57 | 58 | 59 | module.exports = config; -------------------------------------------------------------------------------- /src/config/staging.env: -------------------------------------------------------------------------------- 1 | APP_NAME="Node App" 2 | APP_ENV=staging 3 | APP_DEBUG=true 4 | HOST=0.0.0.0 5 | PORT=4001 6 | 7 | JWT_SECRET=michaeljordan23 8 | JWT_EXPIRE_TIME=3600 9 | 10 | LOG_LEVEL=silly 11 | LOG_GROUP_NAME=node-app 12 | CLOUDWATCH_REGION=ap-southeast-2 13 | CLOUDWATCH_ACCESS_KEY_ID= 14 | CLOUDWATCH_SECRET_ACCESS_KEY= 15 | 16 | DB_CONNECTION=mongo 17 | DB_HOST=node_app_db 18 | DB_PORT=27017 19 | DB_DATABASE_AUTH=admin 20 | DB_DATABASE=node_app 21 | DB_USERNAME=root 22 | DB_PASSWORD=root 23 | 24 | SOCKET_PORT=4002 25 | SOCKET_PATH="/io" 26 | SOCKET_SECURE=false -------------------------------------------------------------------------------- /src/config/test.env: -------------------------------------------------------------------------------- 1 | APP_NAME="Node App" 2 | APP_ENV=test 3 | APP_DEBUG=true 4 | HOST=0.0.0.0 5 | PORT=4001 6 | 7 | JWT_SECRET=michaeljordan23 8 | JWT_EXPIRE_TIME=3600 9 | 10 | LOG_LEVEL=silly 11 | LOG_GROUP_NAME=node-app 12 | CLOUDWATCH_REGION=ap-southeast-2 13 | CLOUDWATCH_ACCESS_KEY_ID= 14 | CLOUDWATCH_SECRET_ACCESS_KEY= 15 | 16 | DB_CONNECTION=mongo 17 | DB_HOST=node_app_db 18 | DB_PORT=27017 19 | DB_DATABASE_AUTH=admin 20 | DB_DATABASE=node_app_test 21 | DB_USERNAME=root 22 | DB_PASSWORD=root 23 | 24 | SOCKET_PORT=4002 25 | SOCKET_PATH="/io" 26 | SOCKET_SECURE=false -------------------------------------------------------------------------------- /src/console/commands/PrintArgs.js: -------------------------------------------------------------------------------- 1 | const print = require('@helpers/PrintHelper'); 2 | const arg = require('@helpers/ArgHelper'); 3 | 4 | class PrintArgs { 5 | run (argv) { 6 | const usage = `./cli -c ` 7 | const options = [ 8 | { name: 'test-arg', alias: 't', type: String } 9 | ]; 10 | const args = arg.parse(usage, options, argv); 11 | print.success(args); 12 | } 13 | } 14 | 15 | module.exports = PrintArgs; -------------------------------------------------------------------------------- /src/console/kernel.js: -------------------------------------------------------------------------------- 1 | const container = require('@containers/cli'); 2 | const arg = require('@helpers/ArgHelper'); 3 | const print = require('@helpers/PrintHelper'); 4 | const routes = require('@routes/console')(container); 5 | 6 | module.exports = () => { 7 | // CONFIG 8 | const usage = `./cli -c ` 9 | const options = [ 10 | { name: 'command', alias: 'c', type: String } 11 | ]; 12 | 13 | const args = arg.parse(usage, options); 14 | 15 | if (!args.command) { 16 | print.error(`Usage: ${usage}`); 17 | process.exit(1); 18 | } 19 | 20 | const { command, otherArgs } = args; 21 | 22 | if (!Object.keys(routes).includes(command)) { 23 | print.error(`Command ${command} is not supported!`); 24 | process.exit(1); 25 | } 26 | 27 | print.debug(`Running command: ${command}`); 28 | 29 | routes[command](otherArgs); 30 | container.dispose(); 31 | } 32 | -------------------------------------------------------------------------------- /src/containers/cli.js: -------------------------------------------------------------------------------- 1 | // LIBS 2 | const awilix = require("awilix"); 3 | const asClass = awilix.asClass; 4 | const InjectionMode = awilix.InjectionMode; 5 | const Lifetime = awilix.Lifetime; 6 | 7 | const container = awilix.createContainer({ 8 | injectionMode: InjectionMode.CLASSIC 9 | }); 10 | 11 | // Services 12 | container.register({ 13 | loggerService: asClass(require('@services/LoggerService'), { 14 | lifetime: Lifetime.SINGLETON, 15 | injectionMode: InjectionMode.CLASSIC 16 | }) 17 | }); 18 | 19 | 20 | // Commands 21 | container.register({ 22 | printArgs: asClass(require('@console/commands/PrintArgs'), { 23 | lifetime: Lifetime.SINGLETON, 24 | injectionMode: InjectionMode.CLASSIC 25 | }) 26 | }); 27 | 28 | 29 | module.exports = container; -------------------------------------------------------------------------------- /src/containers/test.js: -------------------------------------------------------------------------------- 1 | // LIBS 2 | const awilix = require("awilix"); 3 | const asClass = awilix.asClass; 4 | const asFunction = awilix.asFunction; 5 | const InjectionMode = awilix.InjectionMode; 6 | const Lifetime = awilix.Lifetime; 7 | 8 | const container = awilix.createContainer({ 9 | injectionMode: InjectionMode.CLASSIC 10 | }); 11 | 12 | 13 | // REPOSITORIES 14 | container.register({ 15 | iuserRepository: asClass( 16 | require("@repositories/Vendor/MongoDb/UserRepository"), 17 | { 18 | lifetime: Lifetime.SINGLETON, 19 | injectionMode: InjectionMode.CLASSIC 20 | } 21 | ) 22 | }); 23 | 24 | 25 | // SERVICES 26 | container.register({ 27 | userService: asClass(require("@services/UserService"), { 28 | lifetime: Lifetime.SINGLETON, 29 | injectionMode: InjectionMode.CLASSIC 30 | }) 31 | }); 32 | 33 | container.register({ 34 | loggerService: asClass(require("@services/LoggerService"), { 35 | lifetime: Lifetime.SINGLETON, 36 | injectionMode: InjectionMode.CLASSIC 37 | }) 38 | }); 39 | 40 | 41 | // MIDDLEWARE 42 | container.register({ 43 | ContentTypeHandler: asClass(require("@middlewares/ContentTypeHandler"), { 44 | lifetime: Lifetime.SINGLETON, 45 | injectionMode: InjectionMode.CLASSIC 46 | }) 47 | }); 48 | 49 | container.register({ 50 | ErrorResponseMiddleware: asClass( 51 | require("@middlewares/ErrorResponseMiddleware"), 52 | { 53 | lifetime: Lifetime.SINGLETON, 54 | injectionMode: InjectionMode.CLASSIC 55 | } 56 | ) 57 | }); 58 | 59 | container.register({ 60 | AuthMiddleware: asClass(require("@middlewares/AuthMiddleware"), { 61 | lifetime: Lifetime.SINGLETON, 62 | injectionMode: InjectionMode.CLASSIC 63 | }) 64 | }); 65 | 66 | 67 | // CONTROLLERS 68 | container.register({ 69 | mainAPIController: asClass( 70 | require("@controllers/api/MainAPIController"), 71 | { 72 | lifetime: Lifetime.SINGLETON, 73 | injectionMode: InjectionMode.CLASSIC 74 | } 75 | ) 76 | }); 77 | 78 | container.register({ 79 | authenticationAPIController: asClass( 80 | require("@controllers/api/AuthenticationAPIController"), 81 | { 82 | lifetime: Lifetime.SINGLETON, 83 | injectionMode: InjectionMode.CLASSIC 84 | } 85 | ) 86 | }); 87 | 88 | 89 | module.exports = container; -------------------------------------------------------------------------------- /src/containers/web.js: -------------------------------------------------------------------------------- 1 | // LIBS 2 | const awilix = require("awilix"); 3 | const asClass = awilix.asClass; 4 | const asFunction = awilix.asFunction; 5 | const InjectionMode = awilix.InjectionMode; 6 | const Lifetime = awilix.Lifetime; 7 | 8 | const container = awilix.createContainer({ 9 | injectionMode: InjectionMode.CLASSIC 10 | }); 11 | 12 | 13 | // LISTENERS 14 | container.register({ 15 | UserLoggedInEventListener: asClass(require('@listeners/UserLoggedInEventListener'), { 16 | lifetime: Lifetime.SINGLETON, 17 | injectionMode: InjectionMode.CLASSIC 18 | }) 19 | }); 20 | 21 | container.register({ 22 | UserUpdatedEventListener: asClass(require('@listeners/UserUpdatedEventListener'), { 23 | lifetime: Lifetime.SINGLETON, 24 | injectionMode: InjectionMode.CLASSIC 25 | }) 26 | }); 27 | 28 | // REPOSITORIES 29 | container.register({ 30 | iuserRepository: asClass( 31 | require("@repositories/Vendor/MongoDb/UserRepository"), 32 | { 33 | lifetime: Lifetime.SINGLETON, 34 | injectionMode: InjectionMode.CLASSIC 35 | } 36 | ) 37 | }); 38 | 39 | 40 | // SERVICES 41 | container.register({ 42 | userService: asClass(require("@services/UserService"), { 43 | lifetime: Lifetime.SINGLETON, 44 | injectionMode: InjectionMode.CLASSIC 45 | }) 46 | }); 47 | 48 | container.register({ 49 | loggerService: asClass(require("@services/LoggerService"), { 50 | lifetime: Lifetime.SINGLETON, 51 | injectionMode: InjectionMode.CLASSIC 52 | }) 53 | }); 54 | 55 | 56 | // MIDDLEWARE 57 | container.register({ 58 | ContentTypeHandler: asClass(require("@middlewares/ContentTypeHandler"), { 59 | lifetime: Lifetime.SINGLETON, 60 | injectionMode: InjectionMode.CLASSIC 61 | }) 62 | }); 63 | 64 | container.register({ 65 | ErrorResponseMiddleware: asClass( 66 | require("@middlewares/ErrorResponseMiddleware"), 67 | { 68 | lifetime: Lifetime.SINGLETON, 69 | injectionMode: InjectionMode.CLASSIC 70 | } 71 | ) 72 | }); 73 | 74 | container.register({ 75 | AuthMiddleware: asClass(require("@middlewares/AuthMiddleware"), { 76 | lifetime: Lifetime.SINGLETON, 77 | injectionMode: InjectionMode.CLASSIC 78 | }) 79 | }); 80 | 81 | 82 | // CONTROLLERS 83 | container.register({ 84 | mainAPIController: asClass( 85 | require("@controllers/api/MainAPIController"), 86 | { 87 | lifetime: Lifetime.SINGLETON, 88 | injectionMode: InjectionMode.CLASSIC 89 | } 90 | ) 91 | }); 92 | 93 | container.register({ 94 | authenticationAPIController: asClass( 95 | require("@controllers/api/AuthenticationAPIController"), 96 | { 97 | lifetime: Lifetime.SINGLETON, 98 | injectionMode: InjectionMode.CLASSIC 99 | } 100 | ) 101 | }); 102 | 103 | 104 | module.exports = container; -------------------------------------------------------------------------------- /src/controllers/api/AuthenticationAPIController/__test__/currentUser.test.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('chai'); // We use expect to determine case results 2 | 3 | // Mockups that we will use 4 | const seed = { 5 | email: "test@gmail.com", 6 | password: "abcd1234", 7 | username: "test" 8 | }; 9 | let user = null; 10 | 11 | module.exports = (container) => { 12 | const service = container.resolve('userService'); // Resolve user service that will be used inside test case 13 | const controller = container.resolve('authenticationAPIController'); 14 | 15 | before(async () => { 16 | // Before running the cases make sure that we clear user collection so we're sure about the expected behavior 17 | await service.deleteUsers({}); 18 | // Set initial user 19 | const auth = await service.signup({ body: seed }); 20 | user = await service.getUser(auth.userId); 21 | }); 22 | 23 | it("Test authenticated request case", async () => { 24 | // Mockup a http request and bypass set currentUser similar to auth middleware 25 | const { req, res, next } = TestHelper.createExpressMocks({ currentUser: user }); 26 | 27 | // Try to execute currentUser action from controller by using our mockup request 28 | await controller.currentUser(req, res, next); 29 | const data = JSON.parse(res._getData()); 30 | return expect(data).to.exist; 31 | }); 32 | 33 | it("Test unauthenticated request case", async () => { 34 | // In case if we didn't passed auth middleware the request wouldn't have currentUser 35 | const { req, res, next } = TestHelper.createExpressMocks({ }); 36 | 37 | // Try to execute currentUser action from controller by using our mockup request 38 | await controller.currentUser(req, res, next); 39 | // Expected behavior would be that the controller would call next request due auth failure 40 | return expect(next.calledOnce).to.be.true; 41 | }); 42 | 43 | after(async () => { 44 | // Once we finish test cases we clear records from database so it's in clear state again 45 | await service.deleteUsers({}); 46 | }); 47 | }; 48 | -------------------------------------------------------------------------------- /src/controllers/api/AuthenticationAPIController/__test__/index.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This file loads/runs all the tests inside current directory and passes dependency injection container to them. 3 | * It also passes prefix: "-- AuthenticationAPIController#", so all files are described with same prefix, e.g. "-- AuthenticationAPIController#currentUser" 4 | */ 5 | 6 | module.exports = (container) => { 7 | TestHelper.executeTestsInDir(__dirname, container, "-- AuthenticationAPIController#"); 8 | } 9 | -------------------------------------------------------------------------------- /src/controllers/api/AuthenticationAPIController/__test__/login.test.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('chai'); // We use expect to determine case results 2 | 3 | // Mockups that we will use 4 | const testUserData = { 5 | email: "test@gmail.com", 6 | password: "abcd1234", 7 | username: "test" 8 | }; 9 | 10 | module.exports = (container) => { 11 | const service = container.resolve('userService'); // Resolve user service that will be used inside test case 12 | const controller = container.resolve('authenticationAPIController'); 13 | 14 | before(async () => { 15 | // Before running the cases make sure that we clear user collection so we're sure about the expected behavior 16 | await service.deleteUsers({}); 17 | // Set initial user that will be used for login 18 | await service.signup({ body: testUserData }); 19 | }); 20 | 21 | it("Test successfull login with username case", async () => { 22 | const { email, ...body } = testUserData; 23 | // Create a mockup request that has body: { username, password } where the values are taken from testUserData 24 | const { req, res, next } = TestHelper.createExpressMocks({ body }); 25 | 26 | // Simulate request by passing mockup to controller 27 | await controller.signin(req, res, next); 28 | // Expected behavior would be to get a json response 29 | const data = JSON.parse(res._getData()); 30 | return expect(data).to.exist; 31 | }); 32 | 33 | it("Test successfull login with email case", async () => { 34 | // Create a mockup request that has body: { username, password } where the values are taken from testUserData, 35 | // in this case username has email value 36 | const { email, ...body } = testUserData; 37 | body.username = email; 38 | const { req, res, next } = TestHelper.createExpressMocks({ body }); 39 | 40 | // Simulate request by passing mockup to controller 41 | await controller.signin(req, res, next); 42 | // Expected behavior would be to get a json response 43 | const data = JSON.parse(res._getData()); 44 | return expect(data).to.exist; 45 | }); 46 | 47 | it("Test login missign params case", async () => { 48 | // Create an empty request mockup 49 | const { req, res, next } = TestHelper.createExpressMocks({ body: {} }); 50 | 51 | // Simulate request by passing mockup to controller 52 | await controller.signin(req, res, next); 53 | // Expected behavior would be to call the next function inside controller due to failure, 54 | // With this we assure that error is handled properly 55 | return expect(next.calledOnce).to.be.true; 56 | }); 57 | 58 | it("Test login invalid password case", async () => { 59 | const { email, ...body } = testUserData; 60 | body.password = "12345678"; 61 | const { req, res, next } = TestHelper.createExpressMocks({ body }); 62 | 63 | await controller.signin(req, res, next); 64 | return expect(next.calledOnce).to.be.true; 65 | }); 66 | 67 | it("Test login inexistent user case", async () => { 68 | const { email, ...body } = testUserData; 69 | body.username = "notexist"; 70 | const { req, res, next } = TestHelper.createExpressMocks({ body }); 71 | 72 | await controller.signin(req, res, next); 73 | return expect(next.calledOnce).to.be.true; 74 | }); 75 | 76 | after(async () => { 77 | // Once we finish test cases we clear records from database so it's in clear state again 78 | await service.deleteUsers({}); 79 | }); 80 | }; 81 | -------------------------------------------------------------------------------- /src/controllers/api/AuthenticationAPIController/__test__/signup.test.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('chai'); // We use expect to determine case results 2 | 3 | // Mockups that we will use 4 | const testUserData = { 5 | email: "test@gmail.com", 6 | password: "abcd1234", 7 | username: "test" 8 | }; 9 | 10 | module.exports = (container) => { 11 | const service = container.resolve('userService'); // Resolve user service that will be used inside test case 12 | const controller = container.resolve('authenticationAPIController'); 13 | 14 | before(async () => { 15 | // Before running the cases make sure that we clear user collection so we're sure about the expected behavior 16 | await service.deleteUsers({}); 17 | }); 18 | 19 | it("Test successfull signup case", async () => { 20 | // Create a mockup request that has body: { username, password, email } where the values are taken from testUserData 21 | const { req, res, next } = TestHelper.createExpressMocks({ body: testUserData }); 22 | 23 | // Simulate request by passing mockup to controller 24 | await controller.signup(req, res, next); 25 | // Expected behavior would be to get a json response 26 | const data = JSON.parse(res._getData()); 27 | return expect(data).to.exist; 28 | }); 29 | 30 | it("Test signup email/username exists case", async () => { 31 | // Create a mockup request that has body: { username, password, email } where the values are taken from testUserData 32 | // In this case the user is expected to exist due to test above 33 | const { req, res, next } = TestHelper.createExpressMocks({ body: testUserData }); 34 | 35 | // Simulate request by passing mockup to controller 36 | await controller.signup(req, res, next); 37 | // Expected behavior would be to call the next function inside controller due to failure, 38 | // With this we assure that error is handled properly 39 | return expect(next.calledOnce).to.be.true; 40 | }); 41 | 42 | it("Test signup email/username exists case", async () => { 43 | // Create a mockup request that has body: { username, password, email } where the values are taken from testUserData 44 | // In this case the user is expected to exist due to test above 45 | const { req, res, next } = TestHelper.createExpressMocks({ body: testUserData }); 46 | 47 | // Simulate request by passing mockup to controller 48 | await controller.signup(req, res, next); 49 | // Expected behavior would be to call the next function inside controller due to failure, 50 | // With this we assure that error is handled properly 51 | return expect(next.calledOnce).to.be.true; 52 | }); 53 | 54 | it("Test signup missign params case", async () => { 55 | // Create a mockup request with empty body 56 | const { req, res, next } = TestHelper.createExpressMocks({ body: {} }); 57 | 58 | // Simulate request by passing mockup to controller 59 | await controller.signup(req, res, next); 60 | // Expected behavior would be to call the next function inside controller due to failure, 61 | // With this we assure that error is handled properly 62 | return expect(next.calledOnce).to.be.true; 63 | }); 64 | 65 | it("Test signup password numbers only case", async () => { 66 | // Create a mockup request that has password with numbers only 67 | const { req, res, next } = TestHelper.createExpressMocks({ body: { password: "12345678" } }); 68 | 69 | // Simulate request by passing mockup to controller 70 | await controller.signup(req, res, next); 71 | // Expected behavior would be to call the next function inside controller due to failure, 72 | // With this we assure that error is handled properly 73 | return expect(next.calledOnce).to.be.true; 74 | }); 75 | 76 | it("Test signup password letters only case", async () => { 77 | // Create a mockup request that has password with letters only 78 | const { req, res, next } = TestHelper.createExpressMocks({ body: { password: "abcdefgh" } }); 79 | 80 | // Simulate request by passing mockup to controller 81 | await controller.signup(req, res, next); 82 | // Expected behavior would be to call the next function inside controller due to failure, 83 | // With this we assure that error is handled properly 84 | return expect(next.calledOnce).to.be.true; 85 | }); 86 | 87 | it("Test signup password illegal chars case", async () => { 88 | // Create a mockup request that has password with illegal characters 89 | const { req, res, next } = TestHelper.createExpressMocks({ body: { password: " 1a @@@@" } }); 90 | 91 | // Simulate request by passing mockup to controller 92 | await controller.signup(req, res, next); 93 | // Expected behavior would be to call the next function inside controller due to failure, 94 | // With this we assure that error is handled properly 95 | return expect(next.calledOnce).to.be.true; 96 | }); 97 | 98 | it("Test signup password wrong lenght case", async () => { 99 | // Create a mockup request that has password with smaller length than minimal length 100 | const { req, res, next } = TestHelper.createExpressMocks({ body: { password: "abcd123" } }); 101 | 102 | // Simulate request by passing mockup to controller 103 | await controller.signup(req, res, next); 104 | // Expected behavior would be to call the next function inside controller due to failure, 105 | // With this we assure that error is handled properly 106 | return expect(next.calledOnce).to.be.true; 107 | }); 108 | 109 | it("Test signup invalid email case", async () => { 110 | // Create a mockup request that has malformed email 111 | const { req, res, next } = TestHelper.createExpressMocks({ body: { password: "abcd1234", email: "test", username: "test2" } }); 112 | 113 | // Simulate request by passing mockup to controller 114 | await controller.signup(req, res, next); 115 | // Expected behavior would be to call the next function inside controller due to failure, 116 | // With this we assure that error is handled properly 117 | return expect(next.calledOnce).to.be.true; 118 | }); 119 | 120 | after(async () => { 121 | // Once we finish test cases we clear records from database so it's in clear state again 122 | await service.deleteUsers({}); 123 | }); 124 | }; 125 | -------------------------------------------------------------------------------- /src/controllers/api/AuthenticationAPIController/__test__/updatePassword.test.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('chai'); // We use expect to determine case results 2 | 3 | // Mockups 4 | const seed = { 5 | email: "test@gmail.com", 6 | password: "abcd1234", 7 | username: "test" 8 | }; 9 | let user = null; 10 | 11 | module.exports = (container) => { 12 | const service = container.resolve('userService'); 13 | const controller = container.resolve('authenticationAPIController'); 14 | 15 | before(async () => { 16 | // Prepare environment 17 | await service.deleteUsers({}); 18 | const auth = await service.signup({ body: seed }); 19 | user = await service.getUser(auth.userId); 20 | }); 21 | 22 | it("Test successfull password update case", async () => { 23 | // Create a mockup request where we try to change passowrd 24 | const { req, res, next } = TestHelper.createExpressMocks({ 25 | currentUser: user, 26 | body: { 27 | password: "abcd1234", 28 | newPassword: "abcd12345", 29 | confirmPassword: "abcd12345" 30 | } 31 | }); 32 | 33 | // Simulate 34 | await controller.updatePassword(req, res, next); 35 | 36 | // We expect to get json response 37 | const data = JSON.parse(res._getData()); 38 | return expect(data).to.exist; 39 | }); 40 | 41 | it("Test current password mismatch case", async () => { 42 | // Create a mockup request that has incorrect current password 43 | const { req, res, next } = TestHelper.createExpressMocks({ 44 | currentUser: user, 45 | body: { 46 | password: "abcd123424", 47 | newPassword: "abcd12345", 48 | confirmPassword: "abcd12345" 49 | } 50 | }); 51 | 52 | // Simulate 53 | await controller.updatePassword(req, res, next); 54 | 55 | // Expected behavior would be to call the next function inside controller due to failure, 56 | // With this we assure that error is handled properly 57 | return expect(next.calledOnce).to.be.true; 58 | }); 59 | 60 | it("Test invalid new password case", async () => { 61 | // Create a mockup request that has a new password that doesn't pass validation rules 62 | const { req, res, next } = TestHelper.createExpressMocks({ 63 | currentUser: user, 64 | body: { 65 | password: "abcd1234", 66 | newPassword: "abcd12345@", 67 | confirmPassword: "abcd12345@" 68 | } 69 | }); 70 | 71 | // Simulate 72 | await controller.updatePassword(req, res, next); 73 | 74 | // Expected behavior would be to call the next function inside controller due to failure, 75 | // With this we assure that error is handled properly 76 | return expect(next.calledOnce).to.be.true; 77 | }); 78 | 79 | it("Test new password and confirm password mismatch case", async () => { 80 | // Create a mockup request that has password mismatch in new password and confirm new password 81 | const { req, res, next } = TestHelper.createExpressMocks({ 82 | currentUser: user, 83 | body: { 84 | password: "abcd1234", 85 | newPassword: "abcd12345", 86 | confirmPassword: "abcd1234" 87 | } 88 | }); 89 | 90 | // Simulate 91 | await controller.updatePassword(req, res, next); 92 | 93 | // Expected behavior would be to call the next function inside controller due to failure, 94 | // With this we assure that error is handled properly 95 | return expect(next.calledOnce).to.be.true; 96 | }); 97 | 98 | after(async () => { 99 | // Clear the database mess that we caused :) 100 | await service.deleteUsers({}); 101 | }); 102 | }; 103 | -------------------------------------------------------------------------------- /src/controllers/api/AuthenticationAPIController/index.js: -------------------------------------------------------------------------------- 1 | const UserService = require('@services/UserService'); // USED ONLY FOR INTELLISENSE ISSUES 2 | const UserLoggedInEvent = require('@events/UserLoggedInEvent'); 3 | const UserUpdatedEvent = require('@events/UserUpdatedEvent'); 4 | const event = require('@helpers/EventHelper').event; 5 | 6 | class AuthenticationAPIController { 7 | 8 | /** 9 | * @param {UserService} userService 10 | */ 11 | constructor(userService) { 12 | this.userService = userService; 13 | 14 | this.signup = this.signup.bind(this); 15 | this.signin = this.signin.bind(this); 16 | this.currentUser = this.currentUser.bind(this); 17 | this.updatePassword = this.updatePassword.bind(this); 18 | } 19 | 20 | /** 21 | * @api {post} /api/v1/auth/signup Signup 22 | * @apiDescription Used to register as new user 23 | * @apiName Signup 24 | * @apiGroup Authentication 25 | * @apiVersion 0.1.0 26 | * 27 | * @apiParam {String} email Unique email address for user 28 | * @apiParam {String} username Unique username 29 | * @apiParam {String} password Password that contains at least a number and minimum length of 8 characters 30 | * 31 | * @apiSuccess (200) {String} token Authorization token that serves as unique identifier for authorized user 32 | * @apiSuccess (200) {String} userId Unique identifier for user 33 | * @apiSuccess (200) {Number} expires Token expire time 34 | * 35 | * @apiError {String} error Error message explaining the issue 36 | * 37 | * @apiParamExample {json} Request-Example: 38 | * { 39 | * "email": "vigan.abd@gmail.com", 40 | * "password": "abcd1234", 41 | * "username": "vigan" 42 | * } 43 | * 44 | * @apiSuccessExample Success-Response: 45 | * HTTP/1.1 200 OK 46 | * { 47 | * "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiI1YzA3MGUzZWM0ZGU0NDAwY2IxZTBkMTAiLCJpYXQiOjE1NDM5NjYyNzAwMTUsImhhc2giOiI5ZTQ4N2I5OC02NzE4LTRiMTQtYWJmNy00NDMyNDI3ODk0MDQifQ.9VcaYVj8YT0Tgu0ZpP_xjt_5H7Kyco8wSrRdMqfK5X4", 48 | * "userId": "5c070e3ec4de4400cb1e0d10", 49 | * "expires": 3600 50 | * } 51 | * 52 | * @apiErrorExample Error-Response: 53 | * HTTP/1.1 500 Internal Server Error 54 | * { 55 | * "message": "Server Error." 56 | * } 57 | * 58 | */ 59 | async signup(req, res, next) { 60 | try { 61 | const auth = await this.userService.signup(req); 62 | res.statusCode = 200; 63 | return res.json(auth); 64 | } catch (err) { 65 | next(err); 66 | } 67 | } 68 | 69 | /** 70 | * @api {post} /api/v1/auth/login Login 71 | * @apiName Login 72 | * @apiGroup Authentication 73 | * @apiVersion 0.1.0 74 | * 75 | * @apiParam {String} username Unique username or email address 76 | * @apiParam {String} password Password for the user 77 | * 78 | * @apiSuccess (200) {String} token Authorization token that serves as unique identifier for authorized user 79 | * @apiSuccess (200) {String} userId Unique identifier for user 80 | * @apiSuccess (200) {Number} expires Token expire time 81 | * 82 | * @apiError {String} error Error message explaining the issue 83 | * 84 | * @apiParamExample {json} Request-Example: 85 | * { 86 | * "username": "vigan", 87 | * "password": "abcd1234" 88 | * } 89 | * 90 | * @apiSuccessExample Success-Response: 91 | * Same as /signup 92 | * 93 | * @apiErrorExample Error-Response: 94 | * Same as /signup 95 | * 96 | */ 97 | async signin(req, res, next) { 98 | try { 99 | const auth = await this.userService.signin(req); 100 | event(new UserLoggedInEvent(auth.userId)); 101 | res.statusCode = 200; 102 | return res.json(auth); 103 | } catch (err) { 104 | next(err); 105 | } 106 | } 107 | 108 | /** 109 | * @api {get} /api/v1/auth Current User 110 | * @apiName CurrentUser 111 | * @apiGroup Authentication 112 | * @apiVersion 0.1.0 113 | * 114 | * @apiHeader {String} Authorization Authorization token received from /login 115 | * 116 | * @apiSuccess (200) {String} id Unique identifier for user 117 | * @apiSuccess (200) {String} email Unique email address for user 118 | * @apiSuccess (200) {String} username Unique username 119 | * @apiSuccess (200) {String} [password] User password, always null 120 | * @apiSuccess (200) {String} [tokenHash] User token hash, always null 121 | * @apiSuccess (200) {String} [passwordResetToken] User password reset token, always null 122 | * @apiSuccess (200) {String} [passwordResetSentAt] Password reset send date, always null 123 | * @apiSuccess (200) {String} created Date when the user is registered 124 | * 125 | * @apiError {String} error Error message explaining the issue 126 | * 127 | * @apiHeaderExample {json} Header-Example: 128 | * { 129 | * "Authorization": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiI1YmFkZDQ1ZWQ0YTRlNDAwZTVhMjI1YjIiLCJpYXQiOjE1MzgxMzY4NTk0MzcsImhhc2giOiIyZjczMDYxZC0zNWQzLTRlMjAtODkzMS05NWZjOTQyMzkxNmMifQ.92knC28UoiYo_4ogkKj2nH6raHOSK4D40TdhfQb87l8" 130 | * } 131 | * 132 | * @apiSuccessExample Success-Response: 133 | * HTTP/1.1 200 OK 134 | * { 135 | * "id": "5c070e3ec4de4400cb1e0d10", 136 | * "username": "vigan", 137 | * "email": "vigan.abd@gmail.com", 138 | * "password": null, 139 | * "tokenHash": null, 140 | * "passwordResetToken": null, 141 | * "passwordResetSentAt": null, 142 | * "created": "2018-12-04T23:31:10.004Z", 143 | * "updated": "2018-12-04T23:31:10.004Z", 144 | * "lastLogin": "2018-12-04T23:31:10.004Z" 145 | * } 146 | * 147 | * @apiErrorExample Error-Response: 148 | * Same as /signup 149 | * 150 | */ 151 | currentUser(req, res, next) { 152 | try { 153 | res.statusCode = 200; 154 | return res.json(req.currentUser.asDTO()); 155 | } catch (err) { 156 | next(err); 157 | } 158 | } 159 | 160 | /** 161 | * @api {patch} /api/v1/auth/update-password Update Password 162 | * @apiName UpdatePassword 163 | * @apiGroup Authentication 164 | * @apiVersion 0.1.0 165 | * 166 | * @apiHeader {String} Authorization Authorization token received from /login 167 | * 168 | * @apiParam {String} password Current password 169 | * @apiParam {String} newPassword New password for the user 170 | * @apiParam {String} confirmPassword Confirm new password for the user 171 | * 172 | * @apiError {String} error Error message explaining the issue 173 | * 174 | * @apiHeaderExample {json} Header-Example: 175 | * { 176 | * "Authorization": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiI1YmFkZDQ1ZWQ0YTRlNDAwZTVhMjI1YjIiLCJpYXQiOjE1MzgxMzY4NTk0MzcsImhhc2giOiIyZjczMDYxZC0zNWQzLTRlMjAtODkzMS05NWZjOTQyMzkxNmMifQ.92knC28UoiYo_4ogkKj2nH6raHOSK4D40TdhfQb87l8" 177 | * } 178 | * 179 | * @apiParamExample {json} Request-Example: 180 | * { 181 | * "password": "abcd12345", 182 | * "newPassword": "abcd1234", 183 | * "confirmPassword": "abcd1234" 184 | * } 185 | * 186 | * @apiSuccessExample Success-Response: 187 | * Same as /signup 188 | * 189 | * @apiErrorExample Error-Response: 190 | * Same as /signup 191 | * 192 | */ 193 | async updatePassword(req, res, next) { 194 | try { 195 | const auth = await this.userService.updatePassword(req); 196 | event(new UserUpdatedEvent(auth.userId)); 197 | res.statusCode = 200; 198 | return res.json(auth); 199 | } catch (err) { 200 | next(err); 201 | } 202 | } 203 | } 204 | 205 | module.exports = AuthenticationAPIController; -------------------------------------------------------------------------------- /src/controllers/api/MainAPIController/index.js: -------------------------------------------------------------------------------- 1 | const UserService = require('@services/UserService'); // USED ONLY FOR INTELLISENSE ISSUES 2 | 3 | class MainAPIController { 4 | 5 | /** 6 | * @api {get} /api Main route 7 | * @apiName MainRoute 8 | * @apiGroup Main 9 | * @apiVersion 0.1.0 10 | * 11 | * @apiSuccess (200) {String} msg 12 | * 13 | * @apiError {String} error Error message explaining the issue 14 | * 15 | * @apiSuccessExample Success-Response: 16 | * HTTP/1.1 200 OK 17 | * { 18 | * "msg": "App is up and running" 19 | * } 20 | * 21 | * @apiErrorExample Error-Response: 22 | * HTTP/1.1 500 Internal Server Error 23 | * { 24 | * "message": "Server Error." 25 | * } 26 | * 27 | */ 28 | index(req, res, next) { 29 | try { 30 | res.statusCode = 200; 31 | return res.json({ "msg": "App is up and running" }); 32 | } catch (err) { 33 | next(err); 34 | } 35 | } 36 | } 37 | 38 | module.exports = MainAPIController; -------------------------------------------------------------------------------- /src/controllers/web/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vigan-abd/node-api-boilerplate/a6f903b848aa6249446844debe1126d1faeb43c6/src/controllers/web/.gitkeep -------------------------------------------------------------------------------- /src/events/EventBase.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Event base class, each derived class must override signature method 4 | * @class EventBase 5 | */ 6 | class EventBase { 7 | constructor() { 8 | this.signature = '*'; 9 | } 10 | } 11 | 12 | module.exports = EventBase -------------------------------------------------------------------------------- /src/events/UserLoggedInEvent.js: -------------------------------------------------------------------------------- 1 | const EventBase = require('@events/EventBase'); 2 | 3 | class UserLoggedInEvent extends EventBase { 4 | 5 | /** 6 | * @param {String} id - User Id 7 | */ 8 | constructor(id) { 9 | super(); 10 | this.id = id; 11 | 12 | this.signature = 'user-logged-in-event'; 13 | } 14 | } 15 | 16 | module.exports = UserLoggedInEvent; -------------------------------------------------------------------------------- /src/events/UserUpdatedEvent.js: -------------------------------------------------------------------------------- 1 | const EventBase = require('@events/EventBase'); 2 | 3 | class UserUpdatedEvent extends EventBase { 4 | 5 | /** 6 | * @param {String} id - User Id 7 | */ 8 | constructor(id) { 9 | super(); 10 | this.id = id; 11 | 12 | this.signature = 'user-updated-event'; 13 | } 14 | } 15 | 16 | module.exports = UserUpdatedEvent; -------------------------------------------------------------------------------- /src/general/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vigan-abd/node-api-boilerplate/a6f903b848aa6249446844debe1126d1faeb43c6/src/general/.gitkeep -------------------------------------------------------------------------------- /src/helpers/ArgHelper/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @note DO NOT USE THIS HELPER ON CLOUD FUNCTIONS, ONLY ON TESTS 3 | */ 4 | const commandLineArgs = require('command-line-args'); 5 | exports.parse = (usage, options, argv = null) => { 6 | const parseOpts = argv ? {partial: true, argv} : {partial: true}; 7 | const args = commandLineArgs([ 8 | { name: 'help', alias: 'h', type: Boolean }, 9 | ].concat(options), parseOpts); 10 | if(args.help) { 11 | console.log("\x1b[35m%s\x1b[0m", `USAGE >>> ${usage}`); 12 | process.exit(0); 13 | } 14 | args.otherArgs = args._unknown; 15 | return args; 16 | } -------------------------------------------------------------------------------- /src/helpers/DbAdapter/index.js: -------------------------------------------------------------------------------- 1 | const env = require('@helpers/EnvHelper'); 2 | const mongoose = require("mongoose"); 3 | const config = require('@config'); 4 | 5 | mongoose.connect( 6 | config.DB_CONNECTION_STRING, { 7 | user: env('DB_USERNAME', "root"), 8 | pass: env('DB_PASSWORD', "root"), 9 | authSource: env('DB_DATABASE_AUTH', "admin"), 10 | useNewUrlParser: true 11 | } 12 | ); 13 | 14 | module.exports = mongoose; -------------------------------------------------------------------------------- /src/helpers/EnvHelper/index.js: -------------------------------------------------------------------------------- 1 | const env = require('env-var').get; 2 | 3 | /** 4 | * @param {String} key 5 | * @param {any} defaultValue 6 | */ 7 | module.exports = (key, defaultValue) => env(key, defaultValue).asString(); -------------------------------------------------------------------------------- /src/helpers/EventHelper/index.js: -------------------------------------------------------------------------------- 1 | const EmitterBase = require('events'); 2 | const EventBase = require('@events/EventBase'); 3 | 4 | class EventDispatcher extends EmitterBase { 5 | 6 | } 7 | 8 | const emitter = new EventDispatcher(); 9 | 10 | /** 11 | * @param {EventBase} event 12 | */ 13 | const dispatch = (event) => emitter.emit(event.signature, event); 14 | 15 | /** 16 | * @param {String} signature 17 | * @param {Function} listener 18 | */ 19 | const listen = (signature, listener) => emitter.addListener(signature, listener); 20 | 21 | module.exports = { 22 | event: dispatch, 23 | listen 24 | }; -------------------------------------------------------------------------------- /src/helpers/PrintHelper/index.js: -------------------------------------------------------------------------------- 1 | // THESE LIBS ARE USED ONLY FOR COLOR PRINTING 2 | 3 | exports.debug = function (msg) { 4 | console.log("\x1b[35m%s\x1b[0m", `DEBUG >>> ${typeof(msg) == "object" ? JSON.stringify(msg) : msg}`); 5 | }; 6 | 7 | exports.success = function (msg) { 8 | console.log("\x1b[32m%s\x1b[0m", `SUCCESS >>> ${typeof(msg) == "object" ? JSON.stringify(msg) : msg}`); 9 | }; 10 | 11 | exports.error = function (msg) { 12 | console.log("\x1b[31m%s\x1b[0m", `ERROR >>> ${typeof(msg) == "object" ? JSON.stringify(msg) : msg}`); 13 | }; -------------------------------------------------------------------------------- /src/helpers/SecurityHelper/index.js: -------------------------------------------------------------------------------- 1 | const bcrypt = require('bcrypt-nodejs'); 2 | const jwt = require('jwt-simple'); 3 | const uuid = require('uuid'); 4 | const config = require('@config'); 5 | 6 | class SecurityHelper { 7 | /** 8 | * @param {String} id 9 | * @param {String} hash 10 | * @returns {String} 11 | */ 12 | static encryptJwtToken(id, hash) { 13 | const jwtKey = config.JWT_SECRET; 14 | const timestamp = new Date().getTime(); 15 | 16 | return jwt.encode({ 17 | sub: id, 18 | iat: timestamp, 19 | hash: hash 20 | }, jwtKey); 21 | } 22 | 23 | /** 24 | * @param {String} token 25 | * @returns {{sub: String, iat: Number, hash: String}} 26 | */ 27 | static decryptJwtToken(token) { 28 | const jwtKey = config.JWT_SECRET; 29 | try { 30 | return jwt.decode(token, jwtKey); 31 | } catch (err) { 32 | return null; 33 | } 34 | } 35 | 36 | /** 37 | * @param {String} password 38 | * @param {String} hash 39 | * @returns {Promise} 40 | */ 41 | static comparePassword(password, hash) { 42 | return new Promise((resolve, reject) => { 43 | bcrypt.compare(password, hash, (err, res) => { 44 | if (err) reject(err); 45 | resolve(res); 46 | }) 47 | }) 48 | } 49 | 50 | /** 51 | * @param {String} password 52 | * @returns {Promise<{password: String, tokenHash: String}>} 53 | */ 54 | static hashPassword(password) { 55 | const saltRounds = 10; 56 | 57 | return new Promise((resolve, reject) => { 58 | bcrypt.genSalt(saltRounds, (err, salt) => { 59 | if (err) reject(err); 60 | bcrypt.hash(password, salt, null, (_err, passwordHash) => { 61 | if (_err) reject(_err); 62 | const tokenHash = uuid.v4(); 63 | resolve({ 64 | password: passwordHash, 65 | tokenHash: tokenHash 66 | }); 67 | }) 68 | }) 69 | }) 70 | } 71 | 72 | static generateCSRFToken() { 73 | return uuid.v4(); 74 | } 75 | 76 | static checkPasswordPolicy(password) { 77 | let error; 78 | if (!(/\d/).test(password)) 79 | error = 'Password must contain a number.'; 80 | 81 | if (!(/\w/).test(password)) 82 | error = 'Password must contain a letter.'; 83 | 84 | if ((/[^a-zA-Z0-9\_\#\!]/ig).test(password)) 85 | error = 'Password must contain only numbers, letters and the followin chars: _#!'; 86 | 87 | if (!password || password.length < 8) 88 | error = 'Password must be 8 characters.'; 89 | 90 | return error; 91 | } 92 | } 93 | 94 | module.exports = SecurityHelper; -------------------------------------------------------------------------------- /src/helpers/Serializer/index.js: -------------------------------------------------------------------------------- 1 | const o2x = require('object-to-xml'); 2 | 3 | class Serializer { 4 | static toXML(obj) { 5 | try { 6 | obj = 'toJSON' in obj ? obj.toJSON() : obj; 7 | obj = Object.keys(obj).length <= 1 ? obj : { object: obj }; 8 | return o2x({ 9 | '?xml version=\"1.0\" encoding=\"iso-8859-1\"?': null, 10 | ...obj 11 | }); 12 | } catch (ex) { 13 | return {}; 14 | } 15 | } 16 | } 17 | 18 | module.exports = Serializer; -------------------------------------------------------------------------------- /src/helpers/SocketAdapter/index.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const https = require('https'); 3 | const config = require('@config'); 4 | const socketIO = require('socket.io'); 5 | 6 | let srv = config.SOCKET_PORT; 7 | 8 | if (config.SOCKET_SECURE) { 9 | const privateKey = fs.readFileSync(`${__dirname}/../../config/certs/privkey.pem`, 'utf8'); 10 | const certificate = fs.readFileSync(`${__dirname}/../../config/certs/fullchain.pem`, 'utf8'); 11 | const credentials = { key: privateKey, cert: certificate }; 12 | 13 | srv = https.createServer(credentials).listen(config.SOCKET_PORT); 14 | } 15 | 16 | const io = socketIO(srv, { 17 | path: config.SOCKET_PATH 18 | }); 19 | 20 | io.clientCount = 0; 21 | 22 | module.exports = io; -------------------------------------------------------------------------------- /src/helpers/StringHelper/index.js: -------------------------------------------------------------------------------- 1 | const dbAdapter = require('@helpers/DbAdapter'); 2 | 3 | class StringHelper { 4 | 5 | /** 6 | * @param {String} id 7 | */ 8 | static strToObjectId(id) { 9 | return dbAdapter.Types.ObjectId(id); 10 | } 11 | } 12 | 13 | module.exports = StringHelper; -------------------------------------------------------------------------------- /src/helpers/TestHelper/index.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const httpMocks = require('node-mocks-http'); 4 | const sinon = require('sinon'); 5 | const EventEmitter = require('events').EventEmitter; 6 | 7 | /** 8 | * This class is used for loading test files and creating mockups 9 | */ 10 | class TestHelper { 11 | /** 12 | * This method simulates a http request or middleware behavior, it's mostly useful when we test controllers (integration tests) 13 | */ 14 | static createExpressMocks(reqOptions = {}, resOptions = {}, next) { 15 | resOptions.eventEmitter = EventEmitter; 16 | const { req, res } = httpMocks.createMocks(reqOptions, resOptions); 17 | 18 | return { 19 | req, 20 | res, 21 | next: (sinon.spy() || next) 22 | }; 23 | } 24 | 25 | /** 26 | * This method is used to execute tests inside a file. We pass the test case description from outside, 27 | * the filepath from where we want to load tests, 28 | * and options or arguments that we pass to test functions that will be executed. 29 | * @param {String} name test case description 30 | * @param {String} path file path 31 | * @param {*} opts options or arguments passed from outside to test method, e.g. dependency injection container 32 | */ 33 | static importTest(name, path, opts) { 34 | describe(name, () => { 35 | const test = require(path); 36 | if (opts && typeof test === "function") test(opts); 37 | }); 38 | }; 39 | 40 | /** 41 | * This method performs importTest for all files inside a directory except index.test.js 42 | * @param {String} dirname Directory from where we want to executed test files 43 | * @param {*} opts options or arguments passed from outside to test method, e.g. dependency injection container 44 | * @param {String} prefix Test case description prefix, e.g. User Service tests# 45 | */ 46 | static executeTestsInDir(dirname, opts, prefix = "") { 47 | const files = fs.readdirSync(dirname); 48 | files.forEach(file => { 49 | if (file !== 'index.test.js') { 50 | TestHelper.importTest( 51 | `${prefix}${file.replace(".test.js", "")}`, 52 | path.join(dirname, file), 53 | opts 54 | ); 55 | } 56 | }); 57 | } 58 | } 59 | 60 | module.exports = TestHelper 61 | -------------------------------------------------------------------------------- /src/listeners/ListenerBase.js: -------------------------------------------------------------------------------- 1 | class ListenerBase { 2 | /** 3 | * Runs each time an event is emitted 4 | * @param {any} event EventBase type or other data type that will be passed to listener 5 | * @memberof ListenerBase 6 | */ 7 | handle(event) { 8 | 9 | } 10 | } 11 | 12 | module.exports = ListenerBase; -------------------------------------------------------------------------------- /src/listeners/UserLoggedInEventListener.js: -------------------------------------------------------------------------------- 1 | const ListenerBase = require('@listeners/ListenerBase'); 2 | const UserLoggedInEvent = require('@events/UserLoggedInEvent'); 3 | const UserService = require('@services/UserService'); 4 | const LoggerService = require('@services/LoggerService'); 5 | 6 | const { APP_ENV } = require('@config'); 7 | 8 | class UserLoggedInEventListener extends ListenerBase { 9 | /** 10 | * 11 | * @param {UserService} userService 12 | * @param {LoggerService} loggerService 13 | */ 14 | constructor(userService, loggerService) { 15 | super(); 16 | this.userService = userService; 17 | this.loggerService = loggerService; 18 | 19 | this.handle = this.handle.bind(this); 20 | } 21 | 22 | /** 23 | * @param {UserLoggedInEvent} event 24 | */ 25 | async handle(event) { 26 | if (APP_ENV == 'test') return; 27 | 28 | try { 29 | const now = new Date(); 30 | await this.userService.updateStamps(event.id, { 31 | updated: now, lastLogin: now 32 | }); 33 | } catch (ex) { 34 | this.loggerService.log('error', ex, { tags: 'network,remote' }); 35 | } 36 | } 37 | } 38 | 39 | module.exports = UserLoggedInEventListener; -------------------------------------------------------------------------------- /src/listeners/UserUpdatedEventListener.js: -------------------------------------------------------------------------------- 1 | const ListenerBase = require('@listeners/ListenerBase'); 2 | const UserUpdatedEvent = require('@events/UserUpdatedEvent'); 3 | const UserService = require('@services/UserService'); 4 | const LoggerService = require('@services/LoggerService'); 5 | 6 | const { APP_ENV } = require('@config'); 7 | 8 | class UserUpdatedEventListener extends ListenerBase { 9 | /** 10 | * 11 | * @param {UserService} userService 12 | * @param {LoggerService} loggerService 13 | */ 14 | constructor(userService, loggerService) { 15 | super(); 16 | this.userService = userService; 17 | this.loggerService = loggerService; 18 | 19 | this.handle = this.handle.bind(this); 20 | } 21 | 22 | /** 23 | * @param {UserUpdatedEvent} event 24 | */ 25 | async handle(event) { 26 | if (APP_ENV == 'test') return; 27 | 28 | try { 29 | const now = new Date(); 30 | await this.userService.updateStamps(event.id, { 31 | updated: now, lastLogin: now 32 | }); 33 | } catch (ex) { 34 | this.loggerService.log('error', ex, { tags: 'network,remote' }); 35 | } 36 | } 37 | } 38 | 39 | module.exports = UserUpdatedEventListener; -------------------------------------------------------------------------------- /src/middlewares/AuthMiddleware.js: -------------------------------------------------------------------------------- 1 | const UserService = require('@services/UserService'); 2 | 3 | 4 | class AuthMiddleware { 5 | /** 6 | * @param {UserService} userService 7 | */ 8 | constructor(userService) { 9 | this.userService = userService; 10 | 11 | this.authHandler = this.authHandler.bind(this); 12 | this.optionalAuthHandler = this.optionalAuthHandler.bind(this); 13 | } 14 | 15 | /** 16 | * @param {Request} req 17 | * @param {Response} res 18 | * @param {Function} next 19 | */ 20 | async authHandler(req, res, next) { 21 | const token = req.get('authorization'); 22 | 23 | try { 24 | const user = await this.userService.verifyAuthToken(token); 25 | req.currentUser = user; 26 | next(); 27 | } catch (err) { 28 | next(err); 29 | } 30 | } 31 | 32 | /** 33 | * @param {Request} req 34 | * @param {Response} res 35 | * @param {Function} next 36 | */ 37 | async optionalAuthHandler(req, res, next) { 38 | const token = req.get('authorization'); 39 | if (!token) { 40 | next(); 41 | return; 42 | } 43 | 44 | try { 45 | const user = await this.userService.verifyAuthToken(token); 46 | req.currentUser = user; 47 | next(); 48 | } catch (err) { 49 | next(err); 50 | } 51 | } 52 | } 53 | 54 | module.exports = AuthMiddleware; -------------------------------------------------------------------------------- /src/middlewares/ContentTypeHandler.js: -------------------------------------------------------------------------------- 1 | 2 | const Serializer = require('@helpers/Serializer'); 3 | const HttpNotAcceptableException = require('@models/Business/Exeption/HttpNotAcceptableException'); 4 | 5 | class ContentTypeHandler { 6 | /** 7 | * @param {Request} req 8 | * @param {Response} res 9 | * @param {Function} next 10 | */ 11 | handler(req, res, next) { 12 | const { accept = '*/*' } = req.headers; 13 | 14 | if (/^\*/.test(accept) || /(json)|(javascript)/.test(accept)) { 15 | return res.json(res.body); 16 | 17 | } else if (/(xml)|(html)/.test(accept)) { 18 | res.set("Content-Type", 'application/xml'); 19 | return res.send(Serializer.toXML(res.body)); 20 | 21 | } else if (/text/.test(accept)) { 22 | res.set("Content-Type", 'text/plain'); 23 | return res.send(`${Array.isArray(res.body) || typeof res.body === "object" ? 24 | JSON.stringify(res.body) : res.body}`); 25 | 26 | } else { 27 | res.statusCode = 406; 28 | res.statusText = 'Not Acceptable'; 29 | next(new HttpNotAcceptableException(accept)); 30 | } 31 | } 32 | } 33 | 34 | 35 | 36 | module.exports = ContentTypeHandler; -------------------------------------------------------------------------------- /src/middlewares/CorsMiddleware.js: -------------------------------------------------------------------------------- 1 | 2 | const handler = (req, res, next) => { 3 | res.header("Access-Control-Allow-Origin", "*"); 4 | res.header("Access-Control-Allow-Headers", "*"); 5 | res.header("Access-Control-Allow-Methods", "*"); 6 | next(); 7 | }; 8 | 9 | module.exports = handler; -------------------------------------------------------------------------------- /src/middlewares/ErrorResponseMiddleware.js: -------------------------------------------------------------------------------- 1 | const LoggerService = require('@services/LoggerService'); 2 | 3 | class ErrorResponseMiddleware { 4 | /** 5 | * @param {LoggerService} loggerService 6 | */ 7 | constructor(loggerService) { 8 | this.loggerService = loggerService; 9 | 10 | this.handler = this.handler.bind(this); 11 | } 12 | 13 | /** 14 | * @param {any} err 15 | * @param {Request} req 16 | * @param {Response} res 17 | * @param {Function} next 18 | */ 19 | handler(err, req, res, next) { // Don't remove this 'next' 20 | let message = "Server error."; 21 | let status = 500; 22 | 23 | if (err.name == 'ValidationError') { 24 | status = 422; 25 | if (err.errors && Object.keys(err.errors).length > 0) { 26 | let k = Object.keys(err.errors)[0]; 27 | let o = err.errors[k]; 28 | message = o.message; 29 | } 30 | } else if (err.name == 'SimpleMessage') { 31 | message = err.message; 32 | status = err.status; 33 | } else if (err.name == 'AuthenticationError') { 34 | status = 401; 35 | message = "Authentication error!"; 36 | } else if (err.name == 'UserDefined') { 37 | if (err.message) message = err.message; 38 | } 39 | 40 | if (err.statusCode) status = err.statusCode; 41 | 42 | this.loggerService.log('error', err, { tags: 'server' }); 43 | res.statusCode = status; 44 | 45 | return res.json({ message }); 46 | } 47 | } 48 | 49 | module.exports = ErrorResponseMiddleware; -------------------------------------------------------------------------------- /src/middlewares/HttpsMiddleware.js: -------------------------------------------------------------------------------- 1 | 2 | const handler = (req, res, next) => { 3 | if (!req.secure && req.get('x-forwarded-proto') !== 'https') 4 | return res.redirect('https://' + req.get('host') + req.url); 5 | 6 | next(); 7 | }; 8 | 9 | module.exports = handler; -------------------------------------------------------------------------------- /src/models/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vigan-abd/node-api-boilerplate/a6f903b848aa6249446844debe1126d1faeb43c6/src/models/.gitkeep -------------------------------------------------------------------------------- /src/models/Business/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vigan-abd/node-api-boilerplate/a6f903b848aa6249446844debe1126d1faeb43c6/src/models/Business/.gitkeep -------------------------------------------------------------------------------- /src/models/Business/Exeption/HttpNotAcceptableException.js: -------------------------------------------------------------------------------- 1 | class HttpNotAcceptableException extends Error { 2 | constructor(acceptType) { 3 | super(`${acceptType} is/are not supported by http endpoint`); 4 | this.code = 406; 5 | } 6 | } 7 | 8 | module.exports = HttpNotAcceptableException; -------------------------------------------------------------------------------- /src/models/Business/Exeption/InterfaceException.js: -------------------------------------------------------------------------------- 1 | class InterfaceException extends Error { 2 | /** 3 | * @param {String} className 4 | * @param {String} method 5 | */ 6 | constructor(className, method) { 7 | super(`Method ${method} not implemented in class ${className}`); 8 | } 9 | } 10 | 11 | module.exports = InterfaceException; -------------------------------------------------------------------------------- /src/models/Business/Exeption/UserDefinedException.js: -------------------------------------------------------------------------------- 1 | class UserDefinedException { 2 | constructor(message, statusCode) { 3 | this.message = message; 4 | this.statusCode = statusCode; 5 | this.name = 'UserDefined'; 6 | } 7 | } 8 | 9 | module.exports = UserDefinedException; -------------------------------------------------------------------------------- /src/models/Domain/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vigan-abd/node-api-boilerplate/a6f903b848aa6249446844debe1126d1faeb43c6/src/models/Domain/.gitkeep -------------------------------------------------------------------------------- /src/models/Domain/ModelBase.js: -------------------------------------------------------------------------------- 1 | const Joi = require('joi'); 2 | 3 | class ModelBase { 4 | constructor() { 5 | this.rules = Joi.object(); 6 | this.excludedProps = []; 7 | } 8 | 9 | validate() { 10 | return Joi.validate(this.toJSON(), this.rules); 11 | } 12 | 13 | static get DbQuery() { 14 | throw new Error('Method not implemented'); 15 | } 16 | 17 | toJSON() { 18 | const fields = {}; 19 | Object.keys(this).filter(x => !this.excludedProps.includes(x)) 20 | .filter(x => !['rules', 'excludedProps'].includes(x)) 21 | .forEach(x => fields[x] = this[x]); 22 | 23 | return fields; 24 | } 25 | } 26 | 27 | module.exports = ModelBase; -------------------------------------------------------------------------------- /src/models/Domain/User/index.js: -------------------------------------------------------------------------------- 1 | const Joi = require('joi'); 2 | 3 | const ModelBase = require('@models/Domain/ModelBase'); 4 | const DbQuery = require('./schema'); 5 | 6 | class User extends ModelBase { 7 | 8 | /** 9 | * @param {{id: String, username: String, email: String, 10 | password: String, tokenHash: String, passwordResetToken: String, passwordResetSentAt: Date, 11 | created: Date, updated: Date, lastLogin: Date 12 | }} 13 | */ 14 | constructor({ id, username, email, password, tokenHash, passwordResetToken, passwordResetSentAt, created, updated, lastLogin }) { 15 | super(); 16 | this.id = id; 17 | this.username = username; 18 | this.email = email; 19 | this.password = password; 20 | this.tokenHash = tokenHash; 21 | this.passwordResetToken = passwordResetToken; 22 | this.passwordResetSentAt = passwordResetSentAt; 23 | this.created = created; 24 | this.updated = updated; 25 | this.lastLogin = lastLogin; 26 | 27 | this.rules = Joi.object().keys({ 28 | id: Joi.optional().allow(null), 29 | username: Joi.string().required().min(3), 30 | email: Joi.string().required().email(), 31 | password: Joi.string().required(), 32 | tokenHash: Joi.string().required(), 33 | passwordResetToken: Joi.string().optional().allow(null), 34 | passwordResetSentAt: Joi.date().optional().allow(null), 35 | created: Joi.date().optional().allow(null), 36 | updated: Joi.date().optional().allow(null), 37 | lastLogin: Joi.date().optional().allow(null), 38 | }); 39 | } 40 | 41 | asDTO() { 42 | const user = this.toJSON(); 43 | user.password = null; 44 | user.tokenHash = null; 45 | user.passwordResetToken = null; 46 | user.passwordResetSentAt = null; 47 | return user; 48 | } 49 | 50 | static get DbQuery() { 51 | return DbQuery; 52 | } 53 | 54 | } 55 | 56 | module.exports = User; -------------------------------------------------------------------------------- /src/models/Domain/User/schema.js: -------------------------------------------------------------------------------- 1 | const dbAdapter = require('@helpers/DbAdapter'); 2 | const mongoosePaginate = require('mongoose-paginate-v2'); 3 | const uuid = require('uuid'); 4 | 5 | const schema = new dbAdapter.Schema({ 6 | username: { type: String, required: true, unique: true }, 7 | email: { type: String, required: true, unique: true, validate: /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/ }, 8 | password: { type: String, required: true, default: uuid.v4 }, 9 | tokenHash: { type: String, required: true, default: uuid.v4 }, 10 | passwordResetToken: { type: String, required: true, default: uuid.v4 }, 11 | passwordResetSentAt: { type: Date, required: false }, 12 | created: { type: Date, required: true, default: Date.now }, 13 | updated: { type: Date, required: true, default: Date.now }, 14 | lastLogin: { type: Date, required: true, default: Date.now }, 15 | }); 16 | 17 | // Plugins 18 | schema.plugin(mongoosePaginate); 19 | 20 | const Schema = dbAdapter.model('User', schema);; 21 | 22 | module.exports = Schema; -------------------------------------------------------------------------------- /src/repositories/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vigan-abd/node-api-boilerplate/a6f903b848aa6249446844debe1126d1faeb43c6/src/repositories/.gitkeep -------------------------------------------------------------------------------- /src/repositories/Interfaces/IUserRepository.js: -------------------------------------------------------------------------------- 1 | const InterfaceException = require('@models/Business/Exeption/InterfaceException'); 2 | const User = require('@models/Domain/User'); 3 | 4 | 5 | class IUserRepository { 6 | 7 | /** 8 | * 9 | * @param {User} model 10 | * @returns {Promise} 11 | */ 12 | async create(model) { 13 | throw new InterfaceException('IUserRepository', 'create'); 14 | } 15 | 16 | /** 17 | * @param {String} id 18 | * @returns {Promise} 19 | */ 20 | async findById(id) { 21 | throw new InterfaceException('IUserRepository', 'findById'); 22 | } 23 | 24 | /** 25 | * @param {*} conditions 26 | * @returns {Promise} 27 | */ 28 | async findWhere(conditions) { 29 | throw new InterfaceException('IUserRepository', 'findWhere'); 30 | } 31 | 32 | /** 33 | * @param {Number} page 34 | * @param {*} conditions 35 | * @param {*} options 36 | * @returns {Promise<{ 37 | docs: User[], 38 | totalDocs: Number, 39 | limit: Number, 40 | hasPrevPage: Boolean, 41 | hasNextPage: Boolean, 42 | page: Number, 43 | totalPages: Number, 44 | prevPage: String?, 45 | nextPage: String? 46 | }>} 47 | */ 48 | async list(page, conditions = {}, options = {}) { 49 | throw new InterfaceException('IUserRepository', 'list'); 50 | } 51 | 52 | /** 53 | * 54 | * @param {*} conditions 55 | * @returns {Promise} 56 | */ 57 | async count(conditions = null) { 58 | throw new InterfaceException('IUserRepository', 'count'); 59 | } 60 | 61 | /** 62 | * @param {String} id 63 | * @param {*} fields 64 | * @returns {Promise} 65 | */ 66 | async update(id, fields) { 67 | throw new InterfaceException('IUserRepository', 'update'); 68 | } 69 | 70 | /** 71 | * @param {*} conditions 72 | * @returns {Promise} 73 | */ 74 | async remove(conditions) { 75 | throw new InterfaceException('IUserRepository', 'remove'); 76 | } 77 | } 78 | 79 | module.exports = IUserRepository; -------------------------------------------------------------------------------- /src/repositories/Vendor/MongoDb/UserRepository/__test__/create.test.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('chai'); // We use expect to determine case results 2 | const User = require('@models/Domain/User'); 3 | const UserRepository = require('../index'); 4 | 5 | module.exports = (container) => { 6 | const repository = new UserRepository(); 7 | 8 | before(async () => { 9 | // Clear all users so we're sure that it won't fail due to existing users 10 | await repository.remove({}); 11 | }); 12 | 13 | it("Test success create case", async () => { 14 | // Perform create method from vendor repository 15 | const user = await repository.create(new User({ 16 | username: "test", 17 | email: "test@mail.com", 18 | password: "asfohasohuioedshuthetes9ydgnsdbngi" 19 | })); 20 | 21 | // We expect to get a user that has id 22 | return expect(user.id).to.exist; 23 | }); 24 | 25 | after(async () => { 26 | // Clear user once we finish tests 27 | await repository.remove({}); 28 | }); 29 | }; 30 | -------------------------------------------------------------------------------- /src/repositories/Vendor/MongoDb/UserRepository/__test__/index.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This file loads/runs all the tests inside current directory and passes dependency injection container to them. 3 | * It also passes prefix: "-- UserRepository#", so all files are described with same prefix, e.g. "-- UserRepository#create" 4 | */ 5 | 6 | module.exports = (container) => { 7 | TestHelper.executeTestsInDir(__dirname, container, "-- UserRepository#"); 8 | } 9 | -------------------------------------------------------------------------------- /src/repositories/Vendor/MongoDb/UserRepository/index.js: -------------------------------------------------------------------------------- 1 | const IUserRepository = require('@repositories/Interfaces/IUserRepository'); 2 | const User = require('@models/Domain/User'); 3 | 4 | class UserRepository extends IUserRepository { 5 | parseRecord(raw) { 6 | return new User({ 7 | id: raw._id || null, username: raw.username || null, email: raw.email || null, 8 | password: raw.password || null, tokenHash: raw.tokenHash || null, 9 | passwordResetToken: raw.passwordResetToken || null, passwordResetSentAt: raw.passwordResetSentAt || null, 10 | created: raw.created || null, updated: raw.updated || null, lastLogin: raw.lastLogin || null 11 | }); 12 | } 13 | 14 | /** 15 | * 16 | * @param {User} model 17 | * @returns {Promise} 18 | */ 19 | async create(model) { 20 | const res = await User.DbQuery.create(model.toJSON()); 21 | return this.parseRecord(res); 22 | } 23 | 24 | /** 25 | * @param {String} id 26 | * @returns {Promise} 27 | */ 28 | async findById(id) { 29 | const res = await User.DbQuery.findById(id); 30 | return this.parseRecord(res); 31 | } 32 | 33 | /** 34 | * @param {*} conditions 35 | * @returns {Promise} 36 | */ 37 | async findWhere(conditions) { 38 | const res = await User.DbQuery.find(conditions); 39 | return res.map(raw => this.parseRecord(raw)); 40 | } 41 | 42 | /** 43 | * @param {Number} page 44 | * @param {*} conditions 45 | * @param {*} options 46 | * @returns {Promise<{ 47 | docs: User[], 48 | totalDocs: Number, 49 | limit: Number, 50 | hasPrevPage: Boolean, 51 | hasNextPage: Boolean, 52 | page: Number, 53 | totalPages: Number, 54 | prevPage: String?, 55 | nextPage: String? 56 | }>} 57 | */ 58 | async list(page, conditions = {}, options = {}) { 59 | options.page = page; 60 | const res = await User.DbQuery.paginate(conditions, options); 61 | 62 | for (let i = 0; i < res.docs.length; i++) { 63 | res.docs[i].id = res.docs[i]._id; 64 | res.docs[i] = new User(res.docs[i]); 65 | } 66 | res.page = parseInt(res.page); 67 | res.prevPage ? res.prevPage = parseInt(res.prevPage) : null; 68 | res.nextPage ? res.nextPage = parseInt(res.nextPage) : null; 69 | 70 | return res; 71 | } 72 | 73 | /** 74 | * @param {*} conditions 75 | */ 76 | async count(conditions = null) { 77 | return await User.DbQuery.count(conditions || {}); 78 | } 79 | 80 | /** 81 | * @param {String} id 82 | * @param {*} fields 83 | * @returns {Promise} 84 | */ 85 | async update(id, fields) { 86 | await User.DbQuery.findByIdAndUpdate(id, fields); 87 | return await this.findById(id); 88 | } 89 | 90 | /** 91 | * @param {*} conditions 92 | * @returns {Promise} 93 | */ 94 | async remove(conditions) { 95 | await User.DbQuery.deleteMany(conditions); 96 | return true; 97 | } 98 | 99 | query() { 100 | return User.DbQuery; 101 | } 102 | } 103 | 104 | module.exports = UserRepository; -------------------------------------------------------------------------------- /src/repositories/Vendor/Test/UserRepository/index.js: -------------------------------------------------------------------------------- 1 | const seed = require('./seed'); 2 | const IUserRepository = require('@repositories/Interfaces/IUserRepository'); 3 | const UserDefinedException = require("@models/Business/Exeption/UserDefinedException"); 4 | 5 | const User = require('@models/Domain/User'); 6 | 7 | const users = seed(); // Generate seeds 8 | 9 | /** 10 | * This class is a mockup UserRepository, it can be used to perform fast calls instead of making calls to db 11 | * It's mostly useful to speed up testing since we read/write from memory here. 12 | * E.g. it can be used in unit tests in service layer 13 | * 14 | * @class UserRepository 15 | * @extends {IUserRepository} 16 | */ 17 | class UserRepository extends IUserRepository { 18 | // Implement interface 19 | /** 20 | * 21 | * @param {User} model 22 | */ 23 | async create(model) { 24 | // Simulate immediate async 25 | await Promise.resolve(); 26 | model.id = users.length + 1; // Simulate id generator 27 | users.push(model); 28 | return model; 29 | } 30 | 31 | /** 32 | * @param {String} id 33 | */ 34 | async findById(id) { 35 | // Implement full behavior of real repository 36 | await Promise.resolve(); 37 | const user = users.find(x => x.id == id); 38 | if (!user) throw new UserDefinedException("User doesn't exist", 404); 39 | return user; 40 | } 41 | 42 | /** 43 | * @param {*} conditions 44 | */ 45 | async findWhere(conditions) { 46 | // Simply return all users 47 | await Promise.resolve() 48 | return users; 49 | } 50 | 51 | /** 52 | * @param {Number} page 53 | * @param {*} conditions 54 | * @param {*} options 55 | */ 56 | async list(page, conditions = {}, options = {}) { 57 | // Create exact model as in pagination model 58 | await Promise.resolve(); 59 | return { 60 | docs: users, 61 | totalDocs: users.length, 62 | limit: users.length, 63 | hasPrevPage: false, 64 | hasNextPage: false, 65 | page: 1, 66 | totalPages: 1, 67 | prevPage: null, 68 | nextPage: null 69 | } 70 | } 71 | 72 | /** 73 | * @param {*} conditions 74 | */ 75 | async count(conditions = null) { 76 | // Simulate db count behavior 77 | await Promise.resolve(); 78 | return users.length; 79 | } 80 | 81 | /** 82 | * @param {String} id 83 | * @param {*} fields 84 | */ 85 | async update(id, fields) { 86 | // Simulate update against data that is stored in memory 87 | await Promise.resolve(); 88 | const index = users.findIndex(x => x.id == id); 89 | if (index < 0) throw new UserDefinedException("User doesn't exist", 404); 90 | for (const key in fields) { 91 | users[index][key] = fields[key]; 92 | } 93 | return users[index]; 94 | } 95 | 96 | /** 97 | * @param {*} conditions 98 | * @returns {Promise} 99 | */ 100 | async remove(conditions) { 101 | // Simply remove last item 102 | await Promise.resolve(); 103 | users.pop(); 104 | return true; 105 | } 106 | 107 | reset() { 108 | users = seed(); 109 | } 110 | } 111 | 112 | module.exports = UserRepository; -------------------------------------------------------------------------------- /src/repositories/Vendor/Test/UserRepository/seed.js: -------------------------------------------------------------------------------- 1 | 2 | const User = require('@models/Domain/User'); 3 | 4 | // Simple seed generator 5 | module.exports = () => [ 6 | new User({ 7 | "id": "5c085c669b12c7002a8321eb", 8 | "username": "vigan2", 9 | "email": "vigan2@mail.com", 10 | "password": null, 11 | "tokenHash": null, 12 | "passwordResetToken": null, 13 | "passwordResetSentAt": null, 14 | "created": "2018-12-05T23:16:54.829Z", 15 | "updated": "2018-12-05T00:28:07.533Z", 16 | "lastLogin": "2018-12-04T23:31:10.004Z" 17 | }), 18 | new User({ 19 | "id": "5c071b97dcf6820403f6d97b", 20 | "username": "vigan.abd", 21 | "email": "vig.an.abd@gmail.com", 22 | "password": null, 23 | "tokenHash": null, 24 | "passwordResetToken": null, 25 | "passwordResetSentAt": null, 26 | "created": "2018-12-05T00:28:07.533Z", 27 | "updated": "2018-12-05T00:28:07.533Z", 28 | "lastLogin": "2018-12-05T00:28:07.533Z" 29 | }) 30 | ]; -------------------------------------------------------------------------------- /src/routes/api/index.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const router = express.Router(); 3 | const RouteBuilder = require("simple-express-route-builder"); 4 | 5 | const register = container => { 6 | // MIDDLEWARES 7 | const authMiddleware = container.resolve("AuthMiddleware"); 8 | 9 | // CONTROLLERS 10 | const authenticationAPIController = container.resolve("authenticationAPIController"); 11 | const mainAPIController = container.resolve("mainAPIController"); 12 | 13 | const builder = new RouteBuilder('/api', router); 14 | 15 | builder.use((group, action) => action('GET', mainAPIController.index)); 16 | 17 | builder.use((group, action) => 18 | group("/v1/auth", [ 19 | action("GET", [authMiddleware.authHandler], authenticationAPIController.currentUser), 20 | group("/signup", [ 21 | action("POST", authenticationAPIController.signup) 22 | ]), 23 | group("/login", [ 24 | action("POST", authenticationAPIController.signin) 25 | ]), 26 | group("/update-password", [authMiddleware.authHandler], [ 27 | action("PATCH", authenticationAPIController.updatePassword) 28 | ]) 29 | ]) 30 | ); 31 | 32 | return router; 33 | }; 34 | 35 | module.exports = register; 36 | -------------------------------------------------------------------------------- /src/routes/console/index.js: -------------------------------------------------------------------------------- 1 | const register = (container) => { 2 | const printArgs = container.resolve('printArgs'); 3 | 4 | return { 5 | 'console:print-args': printArgs.run 6 | }; 7 | } 8 | 9 | module.exports = register; -------------------------------------------------------------------------------- /src/routes/event/index.js: -------------------------------------------------------------------------------- 1 | const { listen } = require('@helpers/EventHelper'); 2 | const UserLoggedInEvent = require('@events/UserLoggedInEvent'); 3 | const UserUpdatedEvent = require('@events/UserUpdatedEvent'); 4 | 5 | const register = (container) => { 6 | // SIGNATURES 7 | const USER_LOGGED_IN_EVENT = new UserLoggedInEvent().signature; 8 | const USER_UPDATED_EVENT = new UserUpdatedEvent().signature; 9 | 10 | // LISTENERS 11 | listen(USER_LOGGED_IN_EVENT, container.resolve('UserLoggedInEventListener').handle); 12 | listen(USER_UPDATED_EVENT, container.resolve('UserUpdatedEventListener').handle); 13 | } 14 | 15 | module.exports = register; -------------------------------------------------------------------------------- /src/routes/index.js: -------------------------------------------------------------------------------- 1 | const api = require('./api'); 2 | const event = require('./event'); 3 | const jobs = require('./jobs'); 4 | const socket = require('./socket'); 5 | const web = require('./web'); 6 | 7 | module.exports = { 8 | api, 9 | event, 10 | jobs, 11 | socket, 12 | web, 13 | }; 14 | -------------------------------------------------------------------------------- /src/routes/jobs/index.js: -------------------------------------------------------------------------------- 1 | const cron = require('node-cron'); 2 | 3 | const register = (container) => { 4 | const loggerService = container.resolve('loggerService'); 5 | 6 | cron.schedule('* * * * *', async () => { 7 | loggerService.log("info", `Ping from cron job`); 8 | }); 9 | } 10 | 11 | module.exports = register; -------------------------------------------------------------------------------- /src/routes/socket/index.js: -------------------------------------------------------------------------------- 1 | const io = require('@helpers/SocketAdapter'); 2 | 3 | const register = (container) => { 4 | const loggerService = container.resolve('loggerService'); 5 | 6 | io.on('connect', socket => { 7 | io.clientCount++; 8 | loggerService.log("info", `Client #${socket.id} connected, total connections: ${io.clientCount}`); 9 | 10 | socket.on('disconnect', () => { 11 | io.clientCount--; 12 | loggerService.log("info", `Client #${socket.id} disconnected, total connections: ${io.clientCount}`); 13 | }); 14 | }); 15 | } 16 | 17 | module.exports = register; -------------------------------------------------------------------------------- /src/routes/web/index.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const router = express.Router(); 3 | 4 | const register = (container) => { 5 | return router; 6 | } 7 | 8 | module.exports = register; -------------------------------------------------------------------------------- /src/services/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vigan-abd/node-api-boilerplate/a6f903b848aa6249446844debe1126d1faeb43c6/src/services/.gitkeep -------------------------------------------------------------------------------- /src/services/LoggerService/index.js: -------------------------------------------------------------------------------- 1 | const winston = require('winston'); 2 | const CloudWatchTransport = require('winston-aws-cloudwatch'); 3 | const config = require('@config'); 4 | 5 | class LoggerService { 6 | 7 | constructor() { 8 | const logger = new winston.Logger(); 9 | 10 | logger.on('error', err => { 11 | console.log(err); 12 | }); 13 | 14 | if (config.APP_ENV == 'development' || config.APP_ENV == 'staging') { 15 | logger.add(winston.transports.Console, { timestamp: true, colorize: true }); 16 | } 17 | 18 | if (config.APP_ENV == 'staging' || config.APP_ENV == 'production') { 19 | logger.add(winston.transports.File, { filename: `${config.ROOT}/../app.log` }); 20 | } 21 | 22 | if (config.APP_ENV == 'production') { 23 | const logConfig = { 24 | logGroupName: config.LOG_GROUP_NAME, // REQUIRED 25 | logStreamName: config.APP_ENV, // REQUIRED 26 | createLogGroup: false, 27 | createLogStream: true, 28 | awsConfig: { 29 | accessKeyId: config.CLOUDWATCH_ACCESS_KEY_ID, 30 | secretAccessKey: config.CLOUDWATCH_SECRET_ACCESS_KEY, 31 | region: config.CLOUDWATCH_REGION 32 | }, 33 | formatLog: item => { 34 | return item.level + ': ' + item.message + ' ' + JSON.stringify(item.meta); 35 | } 36 | }; 37 | 38 | logger.add(CloudWatchTransport, logConfig); 39 | } 40 | 41 | logger.level = config.LOG_LEVEL; 42 | 43 | logger.stream = { 44 | write: message => { 45 | logger.info(message); 46 | } 47 | }; 48 | 49 | this.logger = logger; 50 | this.stream = logger.stream; 51 | this.log = this.log.bind(this); 52 | } 53 | 54 | log(type, message, tags) { 55 | const { logger } = this; 56 | 57 | logger.log(type, message, tags); 58 | } 59 | }; 60 | 61 | 62 | module.exports = LoggerService; -------------------------------------------------------------------------------- /src/services/UserService/__test__/getUser.test.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('chai'); // We use expect to determine case results 2 | const UserRepository = require('@repositories/Vendor/Test/UserRepository'); 3 | 4 | module.exports = (container) => { 5 | const service = container.resolve('userService'); 6 | const repository = container.resolve('iuserRepository'); 7 | 8 | before(() => { 9 | // Replace real repository with mockup repository so we speed up testing 10 | // Real repository performs calls against db, mockup against memory so it's faster and our focus is in service behavior, 11 | // not repository behavior 12 | service.iuserRepository = new UserRepository(); 13 | }) 14 | 15 | it("Test success getUser case", async () => { 16 | // Test case when we get existing user (see seed.js in repository/Vendor/Test/UserRepository) for dataset 17 | const user = await service.getUser("5c085c669b12c7002a8321eb"); 18 | return expect(user.id).to.exist; 19 | }); 20 | 21 | after(() => { 22 | // Replace repository mockup with real one once we finish 23 | service.iuserRepository = repository; 24 | }); 25 | }; 26 | -------------------------------------------------------------------------------- /src/services/UserService/__test__/index.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This file loads/runs all the tests inside current directory and passes dependency injection container to them. 3 | * It also passes prefix: "-- UserService#", so all files are described with same prefix, e.g. "-- UserService#create" 4 | */ 5 | 6 | module.exports = (container) => { 7 | TestHelper.executeTestsInDir(__dirname, container, "-- UserService#"); 8 | } 9 | -------------------------------------------------------------------------------- /src/services/UserService/index.js: -------------------------------------------------------------------------------- 1 | const IUserRepository = require('@repositories/Interfaces/IUserRepository'); 2 | const UserDefinedException = require('@models/Business/Exeption/UserDefinedException'); 3 | const User = require('@models/Domain/User'); 4 | const SecurityHelper = require('@helpers/SecurityHelper'); 5 | const StringHelper = require('@helpers/StringHelper'); 6 | const config = require('@config'); 7 | 8 | 9 | class UserService { 10 | /** 11 | * @param {IUserRepository} iuserRepository 12 | */ 13 | constructor(iuserRepository) { 14 | this.iuserRepository = iuserRepository; 15 | } 16 | 17 | /** 18 | * @param {User} user 19 | */ 20 | authResponse(user) { 21 | const token = SecurityHelper.encryptJwtToken(user.id, user.tokenHash); 22 | 23 | return { 24 | token, 25 | userId: user.id, 26 | expires: parseInt(config.JWT_EXPIRE_TIME) 27 | }; 28 | } 29 | 30 | /** 31 | * @param {String} token 32 | */ 33 | async verifyAuthToken(token) { 34 | const decryptedToken = SecurityHelper.decryptJwtToken(token); 35 | if (!decryptedToken) throw new UserDefinedException("Authorization missing.", 401); 36 | if (!decryptedToken.sub) throw new UserDefinedException("Authorization missing.", 401); 37 | if (new Date().getTime() - decryptedToken.iat > (config.JWT_EXPIRE_TIME * 1000)) 38 | throw new UserDefinedException("Your session has expired! Please sign-in again.", 401); 39 | 40 | const user = await this.iuserRepository.findById(decryptedToken.sub); 41 | if (!user) 42 | throw new UserDefinedException("Your user does not exist .", 401); 43 | 44 | if (user.tokenHash != decryptedToken.hash) 45 | throw new UserDefinedException("Your session has expired! Please sign-in again.", 401); 46 | return user; 47 | } 48 | 49 | /** 50 | * @param {Request} req 51 | */ 52 | async signup(req) { 53 | const email = req.body.email ? req.body.email.toLowerCase() : null; 54 | const username = req.body.username ? req.body.username.toLowerCase() : null; 55 | const password = req.body.password || null; 56 | 57 | const passwordError = SecurityHelper.checkPasswordPolicy(password); 58 | if (passwordError) 59 | throw new UserDefinedException(passwordError, 422); 60 | 61 | const count = await this.iuserRepository.count({ $or: [{ email }, { username }] }); 62 | if (count > 0) 63 | throw new UserDefinedException("Email or username is in use.", 409); 64 | 65 | const pass = await SecurityHelper.hashPassword(password); 66 | const model = new User({ 67 | username: username, 68 | email: email, 69 | password: pass.password, 70 | tokenHash: pass.tokenHash, 71 | }); 72 | 73 | const { error } = model.validate(); 74 | if (error) 75 | throw new UserDefinedException(error.message, 422); 76 | 77 | const user = await this.iuserRepository.create( 78 | new User({ 79 | username: username, 80 | email: email, 81 | password: pass.password, 82 | tokenHash: pass.tokenHash, 83 | })); 84 | return this.authResponse(user); 85 | } 86 | 87 | /** 88 | * @param {Request} req 89 | */ 90 | async signin(req) { 91 | const username = req.body.username || null; 92 | const password = req.body.password || null; 93 | 94 | if (!username || !password) 95 | throw new UserDefinedException("Email and password are required!", 404); 96 | 97 | let user; 98 | 99 | const users = await this.iuserRepository.findWhere({ $or: [{ username }, { email: username }] }); 100 | if (users.length != 1) 101 | throw new UserDefinedException("Incorrect email or password.", 401); 102 | user = users[0]; 103 | 104 | const isMatch = await SecurityHelper.comparePassword(password, user.password); 105 | if (!isMatch) 106 | throw new UserDefinedException("Incorrect email or password", 401); 107 | 108 | return this.authResponse(user); 109 | } 110 | 111 | /** 112 | * @param {Request} req 113 | */ 114 | async updatePassword(req) { 115 | const password = req.body.password || null; // CURRENT PASSWORD 116 | const newPassword = req.body.newPassword || null; 117 | const confirmPassword = req.body.confirmPassword || null; 118 | 119 | if (!newPassword || !confirmPassword || !password) 120 | throw new UserDefinedException("Fields password, newPassword, and confirmPassword are required!", 422); 121 | 122 | if (newPassword !== confirmPassword) 123 | throw new UserDefinedException("New password and confirmed new password mismatch", 422); 124 | 125 | const user = req.currentUser; 126 | 127 | const isMatch = await SecurityHelper.comparePassword(password, user.password); 128 | if (!isMatch) 129 | throw new UserDefinedException("Password mismatch!", 401); 130 | 131 | const passwordError = SecurityHelper.checkPasswordPolicy(newPassword); 132 | if (passwordError) 133 | throw new UserDefinedException(passwordError, 422); 134 | 135 | const pass = await SecurityHelper.hashPassword(newPassword); 136 | const res = await this.iuserRepository.update(user.id, pass); 137 | return this.authResponse(res); 138 | } 139 | 140 | /** 141 | * @param {String} userId 142 | */ 143 | getUser(userId) { 144 | return this.iuserRepository.findById(userId); 145 | } 146 | 147 | /** 148 | * @param {*} conditions 149 | */ 150 | deleteUsers(conditions) { 151 | return this.iuserRepository.remove(conditions); 152 | } 153 | 154 | /** 155 | * @param {String} id 156 | * @param {{updated?: Date, lastLogin?: Date}} fields 157 | */ 158 | updateStamps(id, fields) { 159 | const stamps = {}; 160 | if (fields.updated) stamps['updated'] = fields.updated; 161 | if (fields.lastLogin) stamps['lastLogin'] = fields.lastLogin; 162 | return this.iuserRepository.update(id, stamps); 163 | } 164 | } 165 | 166 | module.exports = UserService; -------------------------------------------------------------------------------- /src/tests/index.test.js: -------------------------------------------------------------------------------- 1 | require('module-alias/register'); 2 | const container = require('@containers/test'); // Load awilix container for test environment 3 | const dbAdapter = require('@helpers/DbAdapter'); 4 | const TestHelper = require('@helpers/TestHelper'); 5 | 6 | global.TestHelper = TestHelper; // Make TestHelper globally available for all tests so we don't need to require it manually 7 | 8 | // Initialization point 9 | before(() => { 10 | console.log("*** Starting tests! ***"); 11 | }); 12 | 13 | 14 | // Tests 15 | 16 | // Unit tests 17 | describe("*** Unit testing! ***", () => { 18 | // Specify folders inside src/repositories dir that we want to load 19 | const repositories = [ 20 | "Vendor/MongoDb/UserRepository", 21 | ]; 22 | 23 | // For each repository get the index file and run tests on it, 24 | // other tests will be resolved inside index file automatically via TestHelper.executeTestsInDir 25 | repositories.forEach(r => TestHelper.importTest( 26 | `${r} tests`, // Description, e.g.Vendor/MongoDb/UserRepository tests 27 | `${__dirname}/../repositories/${r}/__test__/index.test`, // path to index file for tests in repository 28 | container // Inject container 29 | )); 30 | 31 | // Specify folders inside src/services dir that we want to load 32 | const services = [ 33 | "UserService", 34 | ]; 35 | 36 | // For each service get the index file and run tests on it, 37 | // other tests will be resolved inside index file automatically via TestHelper.executeTestsInDir 38 | services.forEach(s => TestHelper.importTest( 39 | `${s} tests`, // Description, e.g.UserService tests 40 | `${__dirname}/../services/${s}/__test__/index.test`, // path to index file for tests in service 41 | container // Inject container 42 | )); 43 | }); 44 | 45 | 46 | // Integration tests 47 | describe("*** Integration testing! ***", () => { 48 | // Specify folders inside src/controllers/api dir that we want to load 49 | const controllers = [ 50 | 'AuthenticationAPIController', 51 | ]; 52 | 53 | // For each controller get the index file and run tests on it, 54 | // other tests will be resolved inside index file automatically via TestHelper.executeTestsInDir 55 | controllers.forEach(c => TestHelper.importTest( 56 | `${c} tests`, // Description, e.g.UserAPIController tests 57 | `${__dirname}/../controllers/api/${c}/__test__/index.test`, // path to index file for tests in controller 58 | container // Inject container 59 | )); 60 | }); 61 | 62 | 63 | // Destruction point 64 | after(() => { 65 | console.log("*** Tests ended, disposing resources! ***"); 66 | // Once we end tests we release db resource and destroy awilix container 67 | container.dispose(); 68 | dbAdapter.disconnect(); 69 | }); -------------------------------------------------------------------------------- /src/views/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vigan-abd/node-api-boilerplate/a6f903b848aa6249446844debe1126d1faeb43c6/src/views/.gitkeep -------------------------------------------------------------------------------- /start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | APP_CONTAINER="node_app_server" 3 | DB_CONTAINER="node_app_db" 4 | DOCKER_CMD="docker" 5 | DOCKER_COMPOSE_CMD="docker-compose" 6 | echo "Starting Node App - Server >>>" 7 | 8 | $DOCKER_CMD start $APP_CONTAINER 9 | $DOCKER_CMD start $DB_CONTAINER -------------------------------------------------------------------------------- /stop.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | APP_CONTAINER="node_app_server" 3 | DB_CONTAINER="node_app_db" 4 | DOCKER_CMD="docker" 5 | DOCKER_COMPOSE_CMD="docker-compose" 6 | echo "Starting Node App - Server >>>" 7 | 8 | $DOCKER_CMD stop $APP_CONTAINER 9 | $DOCKER_CMD stop $DB_CONTAINER -------------------------------------------------------------------------------- /uninstall.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | APP_CONTAINER="node_app_server" 3 | DB_CONTAINER="node_app_db" 4 | APP_IMG="node_app_server:1.0" 5 | DOCKER_CMD="docker" 6 | DOCKER_COMPOSE_CMD="docker-compose" 7 | 8 | echo "Uninstalling Node App - Server <<<" 9 | $DOCKER_CMD stop $APP_CONTAINER 10 | $DOCKER_CMD rm $APP_CONTAINER 11 | $DOCKER_CMD rmi $APP_IMG 12 | 13 | $DOCKER_CMD stop $DB_CONTAINER 14 | $DOCKER_CMD rm $DB_CONTAINER --------------------------------------------------------------------------------