├── .editorconfig ├── .gitignore ├── .travis.yml ├── Dockerfile ├── LICENSE ├── README.md ├── docker-compose.yml ├── gulpfile.js ├── package-lock.json ├── package.json ├── pm2.json ├── src ├── api │ ├── tasks │ │ ├── index.ts │ │ ├── routes.ts │ │ ├── task-controller.ts │ │ ├── task-validator.ts │ │ └── task.ts │ └── users │ │ ├── index.ts │ │ ├── routes.ts │ │ ├── user-controller.ts │ │ ├── user-validator.ts │ │ └── user.ts ├── configurations │ ├── config.dev.json │ ├── config.test.json │ └── index.ts ├── database.ts ├── index.ts ├── interfaces │ └── request.ts ├── plugins │ ├── interfaces.ts │ ├── jwt-auth │ │ └── index.ts │ ├── logger │ │ └── index.ts │ ├── logging │ │ ├── index.ts │ │ └── logging.ts │ └── swagger │ │ └── index.ts ├── server.ts └── utils │ └── helper.ts ├── test ├── tasks │ └── task-controller-tests.ts ├── users │ └── users-controller-tests.ts └── utils.ts ├── tsconfig.json └── tslint.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | reports 16 | 17 | 18 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 19 | .grunt 20 | 21 | # node-waf configuration 22 | .lock-wscript 23 | 24 | # Compiled binary addons (http://nodejs.org/api/addons.html) 25 | build/Release 26 | 27 | # Dependency directory 28 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 29 | node_modules 30 | 31 | #VSCode 32 | .vscode 33 | 34 | #Ignore build folder 35 | build 36 | 37 | #Ignore typings 38 | typings 39 | 40 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "8" 4 | before_install: 5 | - nvm install "$(jq -r '.engines.node' package.json)" 6 | services: 7 | - mongodb -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mhart/alpine-node:6.9.1 2 | 3 | MAINTAINER Talento90 4 | 5 | # create a specific user to run this container 6 | RUN adduser -S -D user-app 7 | 8 | # add files to container 9 | ADD . /app 10 | 11 | # specify the working directory 12 | WORKDIR app 13 | 14 | RUN chmod -R 777 . 15 | 16 | # build process 17 | RUN npm install 18 | RUN npm run build 19 | RUN npm prune --production 20 | 21 | # run the container using a specific user 22 | USER user-app 23 | 24 | EXPOSE 8080 25 | 26 | # run application 27 | CMD ["npm", "start"] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Talento90 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NodeJS-Hapi TypeScript Scaffolding 2 | 3 | A NodeJS + HapiJS(17) with Typescript Starter kit to build standard projects. 4 | 5 | **Installation** 6 | 7 | * *npm run setup* (install nuget packages & typings) 8 | 9 | **Important Note** 10 | 11 | * If working with NodeJS 10.0.0, Kindly delete the *package.lock.json* file then try *npm install* 12 | 13 | **Run** 14 | 15 | * *gulp build* (build ts files) 16 | * *gulp test* (run mocha tests) 17 | * *gulp tslint* (run tslint) 18 | * *gulp watch* (watch ts files) 19 | * *npm run start* (start the application) 20 | * *npm run watch* (restart the application when files change) 21 | 22 | **Features** 23 | 24 | * *Project Structure - Feature oriented* 25 | * *Hapijs - REST Api* 26 | * *Swagger - documentation* 27 | * *Jwt - authentication* 28 | * *Mongoose - MongoDb* 29 | * *nconf - configurations* 30 | * *Logging - MongoDB collection based logging* 31 | * *Unit Tests - chai + sinon + mocha* 32 | 33 | Running on port 5000 ex: localhost:5000/docs 34 | 35 | Have fun :) 36 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | services: 3 | app: 4 | build: . 5 | environment: 6 | - PORT=8080 7 | - MONGO_URL=mongodb:27017 8 | ports: 9 | - "8080:8080" 10 | links: 11 | - mongodb 12 | mongodb: 13 | image: mongo:latest 14 | ports: 15 | - "27017:27017" -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const gulp = require('gulp'); 4 | const rimraf = require('gulp-rimraf'); 5 | const tslint = require('gulp-tslint'); 6 | const mocha = require('gulp-mocha'); 7 | const shell = require('gulp-shell'); 8 | const env = require('gulp-env'); 9 | 10 | /** 11 | * Remove build directory. 12 | */ 13 | gulp.task('clean', function () { 14 | return gulp.src(outDir, { read: false }) 15 | .pipe(rimraf()); 16 | }); 17 | 18 | /** 19 | * Lint all custom TypeScript files. 20 | */ 21 | gulp.task('tslint', () => { 22 | return gulp.src('src/**/*.ts') 23 | .pipe(tslint({ 24 | formatter: 'prose' 25 | })) 26 | .pipe(tslint.report()); 27 | }); 28 | 29 | /** 30 | * Compile TypeScript. 31 | */ 32 | 33 | function compileTS(args, cb) { 34 | return exec(tscCmd + args, (err, stdout, stderr) => { 35 | console.log(stdout); 36 | 37 | if (stderr) { 38 | console.log(stderr); 39 | } 40 | cb(err); 41 | }); 42 | } 43 | 44 | gulp.task('compile', shell.task([ 45 | 'npm run tsc', 46 | ])) 47 | 48 | /** 49 | * Watch for changes in TypeScript 50 | */ 51 | gulp.task('watch', shell.task([ 52 | 'npm run tsc-watch', 53 | ])) 54 | /** 55 | * Copy config files 56 | */ 57 | gulp.task('configs', (cb) => { 58 | return gulp.src("src/configurations/*.json") 59 | .pipe(gulp.dest('./build/src/configurations')); 60 | }); 61 | 62 | /** 63 | * Build the project. 64 | */ 65 | gulp.task('build', ['tslint', 'compile', 'configs'], () => { 66 | console.log('Building the project ...'); 67 | }); 68 | 69 | /** 70 | * Run tests. 71 | */ 72 | gulp.task('test', ['build'], (cb) => { 73 | const envs = env.set({ 74 | NODE_ENV: 'test' 75 | }); 76 | 77 | gulp.src(['build/test/**/*.js']) 78 | .pipe(envs) 79 | .pipe(mocha({ exit: true })) 80 | .once('error', (error) => { 81 | console.log(error); 82 | process.exit(1); 83 | }); 84 | }); 85 | 86 | gulp.task('default', ['build']); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "typescript-node", 3 | "version": "1.3.1", 4 | "description": "", 5 | "license": "MIT", 6 | "repository": { 7 | "url": "https://github.com/dwyl/hapi-typescript-example.git" 8 | }, 9 | "author": "Talento90", 10 | "keywords": [ 11 | "typescript", 12 | "project structure", 13 | "nodejs" 14 | ], 15 | "scripts": { 16 | "tsc": "tsc", 17 | "tsc-watch": "tsc -w", 18 | "build": "gulp build", 19 | "start": "node build/src/index.js", 20 | "setup": "npm install", 21 | "test": "gulp test", 22 | "watch": "nodemon --watch build/src build/src/index.js" 23 | }, 24 | "dependencies": { 25 | "bcryptjs": "2.4.3", 26 | "boom": "7.2.0", 27 | "eslint": "4.19.0", 28 | "fs": "0.0.1-security", 29 | "good": "8.1.0", 30 | "good-console": "7.1.0", 31 | "good-squeeze": "5.0.2", 32 | "hapi": "17.4.0", 33 | "hapi-auth-jwt2": "8.1.0", 34 | "hapi-swagger": "9.1.1", 35 | "inert": "5.1.0", 36 | "joi": "13.2.0", 37 | "jsonwebtoken": "8.2.1", 38 | "moment": "2.22.1", 39 | "moment-timezone": "0.5.16", 40 | "mongoose": "5.0.16", 41 | "nconf": "0.10.0", 42 | "nodemon": "1.17.3", 43 | "path": "0.12.7", 44 | "rotating-file-stream": "1.3.6", 45 | "vision": "5.3.2" 46 | }, 47 | "devDependencies": { 48 | "@types/bcryptjs": "2.4.1", 49 | "@types/boom": "7.2.0", 50 | "@types/chai": "^4.1.2", 51 | "@types/hapi": "17.0.11", 52 | "@types/joi": "13.0.8", 53 | "@types/jsonwebtoken": "7.2.6", 54 | "@types/mocha": "^2.2.41", 55 | "@types/mongoose": "5.0.10", 56 | "@types/nconf": "0.0.37", 57 | "@types/node": "9.4.7", 58 | "@types/sinon": "^4.1.3", 59 | "chai": "^4.1.2", 60 | "gulp": "3.9.1", 61 | "gulp-env": "0.4.0", 62 | "gulp-mocha": "5.0.0", 63 | "gulp-rimraf": "0.2.2", 64 | "gulp-shell": "0.6.5", 65 | "gulp-tslint": "8.1.3", 66 | "mocha": "^5.0.0", 67 | "node": "9.8.0", 68 | "sinon": "4.4.6", 69 | "tslint": "5.9.1", 70 | "typescript": "^2.7.2" 71 | }, 72 | "engines": { 73 | "node": "8" 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /pm2.json: -------------------------------------------------------------------------------- 1 | { 2 | "apps": [{ 3 | "exec_mode": "cluster", 4 | "instances" : 4, 5 | "script": "./build/src/index.js", 6 | "name": "type-node", 7 | "interpreter": "node", 8 | "env": { 9 | "NODE_ENV": "dev" 10 | } 11 | }] 12 | } -------------------------------------------------------------------------------- /src/api/tasks/index.ts: -------------------------------------------------------------------------------- 1 | import * as Hapi from "hapi"; 2 | import Routes from "./routes"; 3 | import { IDatabase } from "../../database"; 4 | import { IServerConfigurations } from "../../configurations"; 5 | 6 | export function init( 7 | server: Hapi.Server, 8 | configs: IServerConfigurations, 9 | database: IDatabase 10 | ) { 11 | Routes(server, configs, database); 12 | } 13 | -------------------------------------------------------------------------------- /src/api/tasks/routes.ts: -------------------------------------------------------------------------------- 1 | import * as Hapi from "hapi"; 2 | import * as Joi from "joi"; 3 | import TaskController from "./task-controller"; 4 | import * as TaskValidator from "./task-validator"; 5 | import { jwtValidator } from "../users/user-validator"; 6 | import { IDatabase } from "../../database"; 7 | import { IServerConfigurations } from "../../configurations"; 8 | 9 | export default function ( 10 | server: Hapi.Server, 11 | configs: IServerConfigurations, 12 | database: IDatabase 13 | ) { 14 | const taskController = new TaskController(configs, database); 15 | server.bind(taskController); 16 | 17 | server.route({ 18 | method: "GET", 19 | path: "/tasks/{id}", 20 | options: { 21 | handler: taskController.getTaskById, 22 | auth: "jwt", 23 | tags: ["api", "tasks"], 24 | description: "Get task by id.", 25 | validate: { 26 | params: { 27 | id: Joi.string().required() 28 | }, 29 | headers: jwtValidator 30 | }, 31 | plugins: { 32 | "hapi-swagger": { 33 | responses: { 34 | "200": { 35 | description: "Task founded." 36 | }, 37 | "404": { 38 | description: "Task does not exists." 39 | } 40 | } 41 | } 42 | } 43 | } 44 | }); 45 | 46 | server.route({ 47 | method: "GET", 48 | path: "/tasks", 49 | options: { 50 | handler: taskController.getTasks, 51 | auth: "jwt", 52 | tags: ["api", "tasks"], 53 | description: "Get all tasks.", 54 | validate: { 55 | query: { 56 | top: Joi.number().default(5), 57 | skip: Joi.number().default(0) 58 | }, 59 | headers: jwtValidator 60 | } 61 | } 62 | }); 63 | 64 | server.route({ 65 | method: "DELETE", 66 | path: "/tasks/{id}", 67 | options: { 68 | handler: taskController.deleteTask, 69 | auth: "jwt", 70 | tags: ["api", "tasks"], 71 | description: "Delete task by id.", 72 | validate: { 73 | params: { 74 | id: Joi.string().required() 75 | }, 76 | headers: jwtValidator 77 | }, 78 | plugins: { 79 | "hapi-swagger": { 80 | responses: { 81 | "200": { 82 | description: "Deleted Task." 83 | }, 84 | "404": { 85 | description: "Task does not exists." 86 | } 87 | } 88 | } 89 | } 90 | } 91 | }); 92 | 93 | server.route({ 94 | method: "PUT", 95 | path: "/tasks/{id}", 96 | options: { 97 | handler: taskController.updateTask, 98 | auth: "jwt", 99 | tags: ["api", "tasks"], 100 | description: "Update task by id.", 101 | validate: { 102 | params: { 103 | id: Joi.string().required() 104 | }, 105 | payload: TaskValidator.updateTaskModel, 106 | headers: jwtValidator 107 | }, 108 | plugins: { 109 | "hapi-swagger": { 110 | responses: { 111 | "200": { 112 | description: "Deleted Task." 113 | }, 114 | "404": { 115 | description: "Task does not exists." 116 | } 117 | } 118 | } 119 | } 120 | } 121 | }); 122 | 123 | server.route({ 124 | method: "POST", 125 | path: "/tasks", 126 | options: { 127 | handler: taskController.createTask, 128 | auth: "jwt", 129 | tags: ["api", "tasks"], 130 | description: "Create a task.", 131 | validate: { 132 | payload: TaskValidator.createTaskModel, 133 | headers: jwtValidator 134 | }, 135 | plugins: { 136 | "hapi-swagger": { 137 | responses: { 138 | "201": { 139 | description: "Created Task." 140 | } 141 | } 142 | } 143 | } 144 | } 145 | }); 146 | } 147 | -------------------------------------------------------------------------------- /src/api/tasks/task-controller.ts: -------------------------------------------------------------------------------- 1 | import * as Hapi from "hapi"; 2 | import * as Boom from "boom"; 3 | import { ITask } from "./task"; 4 | import { IDatabase } from "../../database"; 5 | import { IServerConfigurations } from "../../configurations"; 6 | import { IRequest } from "../../interfaces/request"; 7 | import { ILogging } from "../../plugins/logging/logging"; 8 | 9 | //Custom helper module 10 | import * as Helper from "../../utils/helper"; 11 | 12 | export default class TaskController { 13 | private database: IDatabase; 14 | private configs: IServerConfigurations; 15 | 16 | constructor(configs: IServerConfigurations, database: IDatabase) { 17 | this.configs = configs; 18 | this.database = database; 19 | } 20 | 21 | public async createTask(request: IRequest, h: Hapi.ResponseToolkit) { 22 | var newTask: ITask = request.payload; 23 | newTask.userId = request.auth.credentials.id; 24 | 25 | try { 26 | let task: ITask = await this.database.taskModel.create(newTask); 27 | return h.response(task).code(201); 28 | } catch (error) { 29 | return Boom.badImplementation(error); 30 | } 31 | } 32 | 33 | public async updateTask(request: IRequest, h: Hapi.ResponseToolkit) { 34 | let userId = request.auth.credentials.id; 35 | let _id = request.params["id"]; 36 | 37 | try { 38 | let task: ITask = await this.database.taskModel.findByIdAndUpdate( 39 | { _id, userId }, //ES6 shorthand syntax 40 | { $set: request.payload }, 41 | { new: true } 42 | ); 43 | 44 | if (task) { 45 | return task; 46 | } else { 47 | return Boom.notFound(); 48 | } 49 | } catch (error) { 50 | return Boom.badImplementation(error); 51 | } 52 | } 53 | 54 | public async deleteTask(request: IRequest, h: Hapi.ResponseToolkit) { 55 | let id = request.params["id"]; 56 | let userId = request["auth"]["credentials"]; 57 | 58 | let deletedTask = await this.database.taskModel.findOneAndRemove({ 59 | _id: id, 60 | userId: userId 61 | }); 62 | 63 | if (deletedTask) { 64 | return deletedTask; 65 | } else { 66 | return Boom.notFound(); 67 | } 68 | } 69 | 70 | public async getTaskById(request: IRequest, h: Hapi.ResponseToolkit) { 71 | let userId = request.auth.credentials.id; 72 | let _id = request.params["id"]; 73 | 74 | let task = await this.database.taskModel.findOne({ _id, userId }) 75 | .lean(true); 76 | 77 | if (task) { 78 | return task; 79 | } else { 80 | return Boom.notFound(); 81 | } 82 | } 83 | 84 | public async getTasks(request: IRequest, h: Hapi.ResponseToolkit) { 85 | let userId = request.auth.credentials.id; 86 | let top = request.query["top"]; 87 | let skip = request.query["skip"]; 88 | let tasks = await this.database.taskModel 89 | .find({ userId: userId }) 90 | .lean(true) 91 | .skip(skip) 92 | .limit(top); 93 | 94 | return tasks; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/api/tasks/task-validator.ts: -------------------------------------------------------------------------------- 1 | import * as Joi from "joi"; 2 | 3 | export const createTaskModel = Joi.object().keys({ 4 | name: Joi.string().required(), 5 | description: Joi.string().required() 6 | }); 7 | 8 | export const updateTaskModel = Joi.object().keys({ 9 | name: Joi.string().required(), 10 | description: Joi.string().required(), 11 | completed: Joi.boolean() 12 | }); 13 | -------------------------------------------------------------------------------- /src/api/tasks/task.ts: -------------------------------------------------------------------------------- 1 | import * as Mongoose from "mongoose"; 2 | 3 | export interface ITask extends Mongoose.Document { 4 | userId: string; 5 | name: string; 6 | description: string; 7 | completed: boolean; 8 | createdAt: Date; 9 | updateAt: Date; 10 | } 11 | 12 | export const TaskSchema = new Mongoose.Schema( 13 | { 14 | userId: { type: String, required: true }, 15 | name: { type: String, required: true }, 16 | description: String, 17 | completed: Boolean 18 | }, 19 | { 20 | timestamps: true 21 | } 22 | ); 23 | 24 | export const TaskModel = Mongoose.model("Task", TaskSchema); 25 | -------------------------------------------------------------------------------- /src/api/users/index.ts: -------------------------------------------------------------------------------- 1 | import * as Hapi from "hapi"; 2 | import Routes from "./routes"; 3 | import { IDatabase } from "../../database"; 4 | import { IServerConfigurations } from "../../configurations"; 5 | 6 | export function init(server: Hapi.Server, configs: IServerConfigurations, database: IDatabase) { 7 | Routes(server, configs, database); 8 | } -------------------------------------------------------------------------------- /src/api/users/routes.ts: -------------------------------------------------------------------------------- 1 | import * as Hapi from "hapi"; 2 | import * as Joi from "joi"; 3 | import UserController from "./user-controller"; 4 | import { UserModel } from "./user"; 5 | import * as UserValidator from "./user-validator"; 6 | import { IDatabase } from "../../database"; 7 | import { IServerConfigurations } from "../../configurations"; 8 | 9 | export default function( 10 | server: Hapi.Server, 11 | serverConfigs: IServerConfigurations, 12 | database: IDatabase 13 | ) { 14 | const userController = new UserController(serverConfigs, database); 15 | server.bind(userController); 16 | 17 | server.route({ 18 | method: "GET", 19 | path: "/users/info", 20 | options: { 21 | handler: userController.infoUser, 22 | auth: "jwt", 23 | tags: ["api", "users"], 24 | description: "Get user info.", 25 | validate: { 26 | headers: UserValidator.jwtValidator 27 | }, 28 | plugins: { 29 | "hapi-swagger": { 30 | responses: { 31 | "200": { 32 | description: "User founded." 33 | }, 34 | "401": { 35 | description: "Please login." 36 | } 37 | } 38 | } 39 | } 40 | } 41 | }); 42 | 43 | server.route({ 44 | method: "DELETE", 45 | path: "/users", 46 | options: { 47 | handler: userController.deleteUser, 48 | auth: "jwt", 49 | tags: ["api", "users"], 50 | description: "Delete current user.", 51 | validate: { 52 | headers: UserValidator.jwtValidator 53 | }, 54 | plugins: { 55 | "hapi-swagger": { 56 | responses: { 57 | "200": { 58 | description: "User deleted." 59 | }, 60 | "401": { 61 | description: "User does not have authorization." 62 | } 63 | } 64 | } 65 | } 66 | } 67 | }); 68 | 69 | server.route({ 70 | method: "PUT", 71 | path: "/users", 72 | options: { 73 | handler: userController.updateUser, 74 | auth: "jwt", 75 | tags: ["api", "users"], 76 | description: "Update current user info.", 77 | validate: { 78 | payload: UserValidator.updateUserModel, 79 | headers: UserValidator.jwtValidator 80 | }, 81 | plugins: { 82 | "hapi-swagger": { 83 | responses: { 84 | "200": { 85 | description: "Updated info." 86 | }, 87 | "401": { 88 | description: "User does not have authorization." 89 | } 90 | } 91 | } 92 | } 93 | } 94 | }); 95 | 96 | server.route({ 97 | method: "POST", 98 | path: "/users", 99 | options: { 100 | handler: userController.createUser, 101 | auth: false, 102 | tags: ["api", "users"], 103 | description: "Create a user.", 104 | validate: { 105 | payload: UserValidator.createUserModel 106 | }, 107 | plugins: { 108 | "hapi-swagger": { 109 | responses: { 110 | "201": { 111 | description: "User created." 112 | } 113 | } 114 | } 115 | } 116 | } 117 | }); 118 | 119 | server.route({ 120 | method: "POST", 121 | path: "/users/login", 122 | options: { 123 | handler: userController.loginUser, 124 | auth: false, 125 | tags: ["api", "users"], 126 | description: "Login a user.", 127 | validate: { 128 | payload: UserValidator.loginUserModel 129 | }, 130 | plugins: { 131 | "hapi-swagger": { 132 | responses: { 133 | "200": { 134 | description: "User logged in." 135 | } 136 | } 137 | } 138 | } 139 | } 140 | }); 141 | } 142 | -------------------------------------------------------------------------------- /src/api/users/user-controller.ts: -------------------------------------------------------------------------------- 1 | import * as Hapi from "hapi"; 2 | import * as Boom from "boom"; 3 | import * as Jwt from "jsonwebtoken"; 4 | import { IUser } from "./user"; 5 | import { IDatabase } from "../../database"; 6 | import { IServerConfigurations } from "../../configurations"; 7 | import { IRequest, ILoginRequest } from "../../interfaces/request"; 8 | 9 | export default class UserController { 10 | private database: IDatabase; 11 | private configs: IServerConfigurations; 12 | 13 | constructor(configs: IServerConfigurations, database: IDatabase) { 14 | this.database = database; 15 | this.configs = configs; 16 | } 17 | 18 | private generateToken(user: IUser) { 19 | const jwtSecret = this.configs.jwtSecret; 20 | const jwtExpiration = this.configs.jwtExpiration; 21 | const payload = { id: user._id }; 22 | 23 | return Jwt.sign(payload, jwtSecret, { expiresIn: jwtExpiration }); 24 | } 25 | 26 | public async loginUser(request: ILoginRequest, h: Hapi.ResponseToolkit) { 27 | const { email, password } = request.payload; 28 | 29 | let user: IUser = await this.database.userModel.findOne({ email: email }); 30 | 31 | if (!user) { 32 | return Boom.unauthorized("User does not exists."); 33 | } 34 | 35 | if (!user.validatePassword(password)) { 36 | return Boom.unauthorized("Password is invalid."); 37 | } 38 | 39 | return { token: this.generateToken(user) }; 40 | } 41 | 42 | public async createUser(request: IRequest, h: Hapi.ResponseToolkit) { 43 | try { 44 | let user: any = await this.database.userModel.create(request.payload); 45 | return h.response({ token: this.generateToken(user) }).code(201); 46 | } catch (error) { 47 | return Boom.badImplementation(error); 48 | } 49 | } 50 | 51 | public async updateUser(request: IRequest, h: Hapi.ResponseToolkit) { 52 | const id = request.auth.credentials.id; 53 | 54 | try { 55 | let user: IUser = await this.database.userModel.findByIdAndUpdate( 56 | id, 57 | { $set: request.payload }, 58 | { new: true } 59 | ); 60 | return user; 61 | } catch (error) { 62 | return Boom.badImplementation(error); 63 | } 64 | } 65 | 66 | public async deleteUser(request: IRequest, h: Hapi.ResponseToolkit) { 67 | const id = request.auth.credentials.id; 68 | let user: IUser = await this.database.userModel.findByIdAndRemove(id); 69 | 70 | return user; 71 | } 72 | 73 | public async infoUser(request: IRequest, h: Hapi.ResponseToolkit) { 74 | const id = request.auth.credentials.id; 75 | let user: IUser = await this.database.userModel.findById(id); 76 | 77 | return user; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/api/users/user-validator.ts: -------------------------------------------------------------------------------- 1 | import * as Joi from "joi"; 2 | 3 | export const createUserModel = Joi.object().keys({ 4 | email: Joi.string().email().trim().required(), 5 | name: Joi.string().required(), 6 | password: Joi.string().trim().required() 7 | }); 8 | 9 | export const updateUserModel = Joi.object().keys({ 10 | email: Joi.string().email().trim(), 11 | name: Joi.string(), 12 | password: Joi.string().trim() 13 | }); 14 | 15 | export const loginUserModel = Joi.object().keys({ 16 | email: Joi.string().email().required(), 17 | password: Joi.string().trim().required() 18 | }); 19 | 20 | export const jwtValidator = Joi.object({'authorization': Joi.string().required()}).unknown(); -------------------------------------------------------------------------------- /src/api/users/user.ts: -------------------------------------------------------------------------------- 1 | import * as Mongoose from "mongoose"; 2 | import * as Bcrypt from "bcryptjs"; 3 | 4 | export interface IUser extends Mongoose.Document { 5 | name: string; 6 | email: string; 7 | password: string; 8 | createdAt: Date; 9 | updateAt: Date; 10 | validatePassword(requestPassword): boolean; 11 | } 12 | 13 | export const UserSchema = new Mongoose.Schema( 14 | { 15 | email: { type: String, unique: true, required: true }, 16 | name: { type: String, required: true }, 17 | password: { type: String, required: true } 18 | }, 19 | { 20 | timestamps: true 21 | } 22 | ); 23 | 24 | function hashPassword(password: string): string { 25 | if (!password) { 26 | return null; 27 | } 28 | 29 | return Bcrypt.hashSync(password, Bcrypt.genSaltSync(8)); 30 | } 31 | 32 | UserSchema.methods.validatePassword = function(requestPassword) { 33 | return Bcrypt.compareSync(requestPassword, this.password); 34 | }; 35 | 36 | UserSchema.pre("save", function(next) { 37 | const user = this; 38 | 39 | if (!user.isModified("password")) { 40 | return next(); 41 | } 42 | 43 | user["password"] = hashPassword(user["password"]); 44 | 45 | return next(); 46 | }); 47 | 48 | UserSchema.pre("findOneAndUpdate", function() { 49 | const password = hashPassword(this.getUpdate().$set.password); 50 | 51 | if (!password) { 52 | return; 53 | } 54 | 55 | this.findOneAndUpdate({}, { password: password }); 56 | }); 57 | 58 | export const UserModel = Mongoose.model("User", UserSchema); 59 | -------------------------------------------------------------------------------- /src/configurations/config.dev.json: -------------------------------------------------------------------------------- 1 | { 2 | "database": { 3 | "connectionString": "mongodb://localhost:27017/taskdb-dev" 4 | }, 5 | "server": { 6 | "port": 5000, 7 | "jwtSecret": "random-secret-password", 8 | "jwtExpiration": "1h", 9 | "routePrefix": "", 10 | "plugins": ["logger", "jwt-auth", "swagger"] 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/configurations/config.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "database": { 3 | "connectionString": "mongodb://localhost:27017/taskdb-test" 4 | }, 5 | "server": { 6 | "port": 5000, 7 | "jwtSecret": "random-secret-password", 8 | "jwtExpiration": "1h", 9 | "routePrefix": "", 10 | "plugins": ["logger", "jwt-auth"] 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/configurations/index.ts: -------------------------------------------------------------------------------- 1 | import * as nconf from "nconf"; 2 | import * as path from "path"; 3 | 4 | //Read Configurations 5 | const configs = new nconf.Provider({ 6 | env: true, 7 | argv: true, 8 | store: { 9 | type: "file", 10 | file: path.join(__dirname, `./config.${process.env.NODE_ENV || "dev"}.json`) 11 | } 12 | }); 13 | 14 | export interface IServerConfigurations { 15 | port: number; 16 | plugins: Array; 17 | jwtSecret: string; 18 | jwtExpiration: string; 19 | routePrefix: string; 20 | } 21 | 22 | export interface IDataConfiguration { 23 | connectionString: string; 24 | } 25 | 26 | export function getDatabaseConfig(): IDataConfiguration { 27 | return configs.get("database"); 28 | } 29 | 30 | export function getServerConfigs(): IServerConfigurations { 31 | return configs.get("server"); 32 | } 33 | -------------------------------------------------------------------------------- /src/database.ts: -------------------------------------------------------------------------------- 1 | import * as Mongoose from "mongoose"; 2 | import { IDataConfiguration } from "./configurations"; 3 | import { ILogging, LoggingModel } from "./plugins/logging/logging"; 4 | import { IUser, UserModel } from "./api/users/user"; 5 | import { ITask, TaskModel } from "./api/tasks/task"; 6 | 7 | export interface IDatabase { 8 | loggingModel: Mongoose.Model; 9 | userModel: Mongoose.Model; 10 | taskModel: Mongoose.Model; 11 | } 12 | 13 | export function init(config: IDataConfiguration): IDatabase { 14 | (Mongoose).Promise = Promise; 15 | Mongoose.connect(process.env.MONGO_URL || config.connectionString); 16 | 17 | let mongoDb = Mongoose.connection; 18 | 19 | mongoDb.on("error", () => { 20 | console.log(`Unable to connect to database: ${config.connectionString}`); 21 | }); 22 | 23 | mongoDb.once("open", () => { 24 | console.log(`Connected to database: ${config.connectionString}`); 25 | }); 26 | 27 | return { 28 | loggingModel: LoggingModel, 29 | taskModel: TaskModel, 30 | userModel: UserModel 31 | }; 32 | } 33 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import * as Server from "./server"; 2 | import * as Database from "./database"; 3 | import * as Configs from "./configurations"; 4 | 5 | console.log(`Running environment ${process.env.NODE_ENV || "dev"}`); 6 | 7 | // Catch unhandling unexpected exceptions 8 | process.on("uncaughtException", (error: Error) => { 9 | console.error(`uncaughtException ${error.message}`); 10 | }); 11 | 12 | // Catch unhandling rejected promises 13 | process.on("unhandledRejection", (reason: any) => { 14 | console.error(`unhandledRejection ${reason}`); 15 | }); 16 | 17 | // Define async start function 18 | const start = async ({ config, db }) => { 19 | try { 20 | const server = await Server.init(config, db); 21 | await server.start(); 22 | console.log("Server running at:", server.info.uri); 23 | } catch (err) { 24 | console.error("Error starting server: ", err.message); 25 | throw err; 26 | } 27 | }; 28 | 29 | // Init Database 30 | const dbConfigs = Configs.getDatabaseConfig(); 31 | const database = Database.init(dbConfigs); 32 | 33 | // Starting Application Server 34 | const serverConfigs = Configs.getServerConfigs(); 35 | 36 | // Start the server 37 | start({ config: serverConfigs, db: database }); 38 | -------------------------------------------------------------------------------- /src/interfaces/request.ts: -------------------------------------------------------------------------------- 1 | import * as Hapi from "hapi"; 2 | 3 | export interface ICredentials extends Hapi.AuthCredentials { 4 | id: string; 5 | } 6 | 7 | export interface IRequestAuth extends Hapi.RequestAuth { 8 | credentials: ICredentials; 9 | } 10 | 11 | export interface IRequest extends Hapi.Request { 12 | auth: IRequestAuth; 13 | } 14 | 15 | export interface ILoginRequest extends IRequest { 16 | payload: { 17 | email: string; 18 | password: string; 19 | }; 20 | } 21 | -------------------------------------------------------------------------------- /src/plugins/interfaces.ts: -------------------------------------------------------------------------------- 1 | import * as Hapi from "hapi"; 2 | import { IDatabase } from "../database"; 3 | import { IServerConfigurations } from "../configurations"; 4 | 5 | export interface IPluginOptions { 6 | database: IDatabase; 7 | serverConfigs: IServerConfigurations; 8 | } 9 | 10 | export interface IPlugin { 11 | register(server: Hapi.Server, options?: IPluginOptions): Promise; 12 | info(): IPluginInfo; 13 | } 14 | 15 | export interface IPluginInfo { 16 | name: string; 17 | version: string; 18 | } 19 | -------------------------------------------------------------------------------- /src/plugins/jwt-auth/index.ts: -------------------------------------------------------------------------------- 1 | import { IPlugin, IPluginOptions } from "../interfaces"; 2 | import * as Hapi from "hapi"; 3 | import { IUser, UserModel } from "../../api/users/user"; 4 | import { IRequest } from "../../interfaces/request"; 5 | 6 | const register = async ( 7 | server: Hapi.Server, 8 | options: IPluginOptions 9 | ): Promise => { 10 | try { 11 | const database = options.database; 12 | const serverConfig = options.serverConfigs; 13 | 14 | const validateUser = async ( 15 | decoded: any, 16 | request: IRequest, 17 | h: Hapi.ResponseToolkit 18 | ) => { 19 | const user = await database.userModel.findById(decoded.id).lean(true); 20 | if (!user) { 21 | return { isValid: false }; 22 | } 23 | 24 | return { isValid: true }; 25 | }; 26 | 27 | await server.register(require("hapi-auth-jwt2")); 28 | 29 | return setAuthStrategy(server, { 30 | config: serverConfig, 31 | validate: validateUser 32 | }); 33 | } catch (err) { 34 | console.log(`Error registering jwt plugin: ${err}`); 35 | throw err; 36 | } 37 | }; 38 | 39 | const setAuthStrategy = async (server, { config, validate }) => { 40 | server.auth.strategy("jwt", "jwt", { 41 | key: config.jwtSecret, 42 | validate, 43 | verifyOptions: { 44 | algorithms: ["HS256"] 45 | } 46 | }); 47 | 48 | server.auth.default("jwt"); 49 | 50 | return; 51 | }; 52 | 53 | export default (): IPlugin => { 54 | return { 55 | register, 56 | info: () => { 57 | return { name: "JWT Authentication", version: "1.0.0" }; 58 | } 59 | }; 60 | }; 61 | -------------------------------------------------------------------------------- /src/plugins/logger/index.ts: -------------------------------------------------------------------------------- 1 | import { IPlugin } from "../interfaces"; 2 | import * as Hapi from "hapi"; 3 | 4 | const register = async (server: Hapi.Server): Promise => { 5 | try { 6 | return server.register({ 7 | plugin: require("good"), 8 | options: { 9 | ops: { 10 | interval: 1000 11 | }, 12 | reporters: { 13 | consoleReporter: [ 14 | { 15 | module: "good-squeeze", 16 | name: "Squeeze", 17 | args: [ 18 | { 19 | error: "*", 20 | log: "*", 21 | response: "*", 22 | request: "*" 23 | } 24 | ] 25 | }, 26 | { 27 | module: "good-console" 28 | }, 29 | "stdout" 30 | ] 31 | } 32 | } 33 | }); 34 | } catch (err) { 35 | console.log(`Error registering logger plugin: ${err}`); 36 | throw err; 37 | } 38 | }; 39 | 40 | export default (): IPlugin => { 41 | return { 42 | register, 43 | info: () => { 44 | return { name: "Good Logger", version: "1.0.0" }; 45 | } 46 | }; 47 | }; 48 | -------------------------------------------------------------------------------- /src/plugins/logging/index.ts: -------------------------------------------------------------------------------- 1 | import * as Hapi from "hapi"; 2 | import { IDatabase } from "../../database"; 3 | import { IServerConfigurations } from "../../configurations"; 4 | 5 | export function init( 6 | server: Hapi.Server, 7 | configs: IServerConfigurations, 8 | database: IDatabase 9 | ) {} 10 | -------------------------------------------------------------------------------- /src/plugins/logging/logging.ts: -------------------------------------------------------------------------------- 1 | import * as Mongoose from "mongoose"; 2 | 3 | export interface ILogging extends Mongoose.Document { 4 | userId: string; //User id for finding the operating user 5 | //userType: string; //User type to finding user role - Decide in future - currently disabled 6 | payload: String; //response or request payload to capture details 7 | response: String; //Status message to capture the reponse 8 | } 9 | 10 | export const LoggingSchema = new Mongoose.Schema( 11 | { 12 | userId: { type: String, required: true }, 13 | payload: { type: String, required: true }, 14 | response: { type: String, required: true } 15 | }, 16 | { 17 | timestamps: true 18 | } 19 | ); 20 | 21 | export const LoggingModel = Mongoose.model("logging", LoggingSchema); 22 | -------------------------------------------------------------------------------- /src/plugins/swagger/index.ts: -------------------------------------------------------------------------------- 1 | import { IPlugin, IPluginInfo } from "../interfaces"; 2 | import * as Hapi from "hapi"; 3 | 4 | const register = async (server: Hapi.Server): Promise => { 5 | try { 6 | return server.register([ 7 | require("inert"), 8 | require("vision"), 9 | { 10 | plugin: require("hapi-swagger"), 11 | options: { 12 | info: { 13 | title: "Task Api", 14 | description: "Task Api Documentation", 15 | version: "1.0" 16 | }, 17 | tags: [ 18 | { 19 | name: "tasks", 20 | description: "Api tasks interface." 21 | }, 22 | { 23 | name: "users", 24 | description: "Api users interface." 25 | } 26 | ], 27 | swaggerUI: true, 28 | documentationPage: true, 29 | documentationPath: "/docs" 30 | } 31 | } 32 | ]); 33 | } catch (err) { 34 | console.log(`Error registering swagger plugin: ${err}`); 35 | } 36 | }; 37 | 38 | export default (): IPlugin => { 39 | return { 40 | register, 41 | info: () => { 42 | return { name: "Swagger Documentation", version: "1.0.0" }; 43 | } 44 | }; 45 | }; 46 | -------------------------------------------------------------------------------- /src/server.ts: -------------------------------------------------------------------------------- 1 | import * as Hapi from "hapi"; 2 | import * as Boom from "boom"; 3 | import { IPlugin } from "./plugins/interfaces"; 4 | import { IServerConfigurations } from "./configurations"; 5 | import * as Logs from "./plugins/logging"; 6 | import * as Tasks from "./api/tasks"; 7 | import * as Users from "./api/users"; 8 | import { IDatabase } from "./database"; 9 | 10 | export async function init( 11 | configs: IServerConfigurations, 12 | database: IDatabase 13 | ): Promise { 14 | try { 15 | const port = process.env.PORT || configs.port; 16 | const server = new Hapi.Server({ 17 | debug: { request: ['error'] }, 18 | port: port, 19 | routes: { 20 | cors: { 21 | origin: ["*"] 22 | } 23 | } 24 | }); 25 | 26 | if (configs.routePrefix) { 27 | server.realm.modifiers.route.prefix = configs.routePrefix; 28 | } 29 | 30 | // Setup Hapi Plugins 31 | const plugins: Array = configs.plugins; 32 | const pluginOptions = { 33 | database: database, 34 | serverConfigs: configs 35 | }; 36 | 37 | let pluginPromises: Promise[] = []; 38 | 39 | plugins.forEach((pluginName: string) => { 40 | var plugin: IPlugin = require("./plugins/" + pluginName).default(); 41 | console.log( 42 | `Register Plugin ${plugin.info().name} v${plugin.info().version}` 43 | ); 44 | pluginPromises.push(plugin.register(server, pluginOptions)); 45 | }); 46 | 47 | await Promise.all(pluginPromises); 48 | 49 | console.log("All plugins registered successfully."); 50 | 51 | console.log("Register Routes"); 52 | Logs.init(server, configs, database); 53 | Tasks.init(server, configs, database); 54 | Users.init(server, configs, database); 55 | console.log("Routes registered sucessfully."); 56 | 57 | return server; 58 | } catch (err) { 59 | console.log("Error starting server: ", err); 60 | throw err; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/utils/helper.ts: -------------------------------------------------------------------------------- 1 | import * as Jwt from "jsonwebtoken"; 2 | import * as Mongoose from "mongoose"; 3 | import { getDatabaseConfig, getServerConfigs, IServerConfigurations } from "../configurations"; 4 | import { LoggingModel } from "../plugins/logging/logging"; 5 | import * as Boom from "boom"; 6 | 7 | let config: any = getServerConfigs(); 8 | 9 | //Database logging async call for storing users logs 10 | export const dbLogger = async (userId: string, payload: string, response: string) => { 11 | 12 | // create a new log 13 | var newLog = new LoggingModel({ userId, payload, response }); 14 | 15 | try { 16 | newLog.save(); 17 | } catch (error) { 18 | console.log("error" + error); 19 | } 20 | }; 21 | 22 | //To generate new JWT token using predefined signatire 23 | export const generateToken = (user: any) => { 24 | console.log(config["jwtSecret"]); 25 | const jwtSecret = config.jwtSecret; 26 | const jwtExpiration = config.jwtExpiration; 27 | const payload = { id: user["_id"] }; 28 | return Jwt.sign(payload, jwtSecret, { expiresIn: jwtExpiration }); 29 | }; 30 | 31 | //To obtain email domain for evaluating office domain 32 | export const checkEmailFormat = (email: string) => { 33 | 34 | // custom regular expression to validate email 35 | let emailRegex: any = /^(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])$/; 36 | let responseSet: any = {}; 37 | 38 | //Preparing response set to customize this method to be more generic. 39 | //Where "statusCode" defines its validity and, 40 | //"StatusMessage" defines the message associated with it. 41 | 42 | try { 43 | let isEmail: boolean = emailRegex.test(email); 44 | if (isEmail) { 45 | responseSet["statusCode"] = isEmail; 46 | responseSet["statusMessage"] = "Email format is valid"; 47 | } else { 48 | responseSet["statusCode"] = isEmail; 49 | responseSet["statusMessage"] = "Email format is not valid"; 50 | } 51 | return responseSet; 52 | } catch (error) { 53 | responseSet["statusCode"] = false; 54 | responseSet["statusMessage"] = "Email format is not valid"; 55 | return responseSet; 56 | } 57 | }; 58 | 59 | //Sort array with key element 60 | export const sortArray = (key: string) => { 61 | return function (a, b) { 62 | if (a[key] > b[key]) { 63 | return 1; 64 | } else if (a[key] < b[key]) { 65 | return -1; 66 | } 67 | return 0; 68 | }; 69 | }; 70 | 71 | 72 | //Sort array with key element 73 | export const removeDuplicatesFromArray = (arr: any, key: string) => { 74 | if (!(arr instanceof Array) || key && typeof key !== 'string') { 75 | return false; 76 | } 77 | 78 | if (key && typeof key === 'string') { 79 | return arr.filter((obj, index, arr) => { 80 | return arr.map(mapObj => mapObj[key]).indexOf(obj[key]) === index; 81 | }); 82 | 83 | } else { 84 | return arr.filter(function (item, index, arr) { 85 | return arr.indexOf(item) === index; 86 | }); 87 | } 88 | }; -------------------------------------------------------------------------------- /test/tasks/task-controller-tests.ts: -------------------------------------------------------------------------------- 1 | import * as chai from "chai"; 2 | import TaskController from "../../src/api/tasks/task-controller"; 3 | import { ITask } from "../../src/api/tasks/task"; 4 | import { IUser } from "../../src/api/users/user"; 5 | import * as Configs from "../../src/configurations"; 6 | import * as Server from "../../src/server"; 7 | import * as Database from "../../src/database"; 8 | import * as Utils from "../utils"; 9 | 10 | const configDb = Configs.getDatabaseConfig(); 11 | const database = Database.init(configDb); 12 | const assert = chai.assert; 13 | const serverConfig = Configs.getServerConfigs(); 14 | 15 | describe("TastController Tests", () => { 16 | let server; 17 | 18 | before(done => { 19 | Server.init(serverConfig, database).then(s => { 20 | server = s; 21 | done(); 22 | }); 23 | }); 24 | 25 | beforeEach(done => { 26 | Utils.createSeedTaskData(database, done); 27 | }); 28 | 29 | afterEach(done => { 30 | Utils.clearDatabase(database, done); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /test/users/users-controller-tests.ts: -------------------------------------------------------------------------------- 1 | import * as chai from "chai"; 2 | import UserController from "../../src/api/users/user-controller"; 3 | import { IUser } from "../../src/api/users/user"; 4 | import * as Configs from "../../src/configurations"; 5 | import * as Server from "../../src/server"; 6 | import * as Database from "../../src/database"; 7 | import * as Utils from "../utils"; 8 | 9 | const configDb = Configs.getDatabaseConfig(); 10 | const database = Database.init(configDb); 11 | const assert = chai.assert; 12 | const serverConfig = Configs.getServerConfigs(); 13 | 14 | describe("UserController Tests", () => { 15 | let server; 16 | 17 | before(done => { 18 | Server.init(serverConfig, database).then(s => { 19 | server = s; 20 | done(); 21 | }); 22 | }); 23 | 24 | beforeEach(done => { 25 | Utils.createSeedUserData(database, done); 26 | }); 27 | 28 | afterEach(done => { 29 | Utils.clearDatabase(database, done); 30 | }); 31 | 32 | it("Create user", async () => { 33 | var user = { 34 | email: "user@mail.com", 35 | name: "John Robot", 36 | password: "123123" 37 | }; 38 | 39 | const res = await server.inject({ 40 | method: "POST", 41 | url: serverConfig.routePrefix + "/users", 42 | payload: user 43 | }); 44 | 45 | var responseBody: any = JSON.parse(res.payload); 46 | assert.equal(201, res.statusCode); 47 | assert.isNotNull(responseBody.token); 48 | }); 49 | 50 | it("Create user invalid data", async () => { 51 | var user = { 52 | email: "user", 53 | name: "John Robot", 54 | password: "123123" 55 | }; 56 | 57 | const res = await server.inject({ 58 | method: "POST", 59 | url: serverConfig.routePrefix + "/users", 60 | payload: user 61 | }); 62 | 63 | assert.equal(400, res.statusCode); 64 | }); 65 | 66 | it("Create user with same email", async () => { 67 | const res = await server.inject({ 68 | method: "POST", 69 | url: serverConfig.routePrefix + "/users", 70 | payload: Utils.createUserDummy() 71 | }); 72 | 73 | assert.equal(500, res.statusCode); 74 | }); 75 | 76 | it("Get user Info", async () => { 77 | var user = Utils.createUserDummy(); 78 | 79 | const loginResponse = await Utils.login(server, serverConfig, user); 80 | assert.equal(200, loginResponse.statusCode); 81 | var login: any = JSON.parse(loginResponse.payload); 82 | 83 | const res = await server.inject({ 84 | method: "GET", 85 | url: serverConfig.routePrefix + "/users/info", 86 | headers: { authorization: login.token } 87 | }); 88 | 89 | var responseBody: IUser = JSON.parse(res.payload); 90 | assert.equal(200, res.statusCode); 91 | assert.equal(user.email, responseBody.email); 92 | }); 93 | 94 | it("Get User Info Unauthorized", async () => { 95 | const res = await server.inject({ 96 | method: "GET", 97 | url: serverConfig.routePrefix + "/users/info", 98 | headers: { authorization: "dummy token" } 99 | }); 100 | 101 | assert.equal(401, res.statusCode); 102 | }); 103 | 104 | it("Delete user", async () => { 105 | var user = Utils.createUserDummy(); 106 | 107 | const loginResponse = await Utils.login(server, serverConfig, user); 108 | assert.equal(200, loginResponse.statusCode); 109 | var login: any = JSON.parse(loginResponse.payload); 110 | 111 | const res = await server.inject({ 112 | method: "DELETE", 113 | url: serverConfig.routePrefix + "/users", 114 | headers: { authorization: login.token } 115 | }); 116 | 117 | assert.equal(200, res.statusCode); 118 | var responseBody: IUser = JSON.parse(res.payload); 119 | assert.equal(user.email, responseBody.email); 120 | 121 | const deletedUser = await database.userModel.findOne({ email: user.email }); 122 | assert.isNull(deletedUser); 123 | }); 124 | 125 | it("Update user info", async () => { 126 | var user = Utils.createUserDummy(); 127 | 128 | const loginResponse = await Utils.login(server, serverConfig, user); 129 | assert.equal(200, loginResponse.statusCode); 130 | var login: any = JSON.parse(loginResponse.payload); 131 | var updateUser = { name: "New Name" }; 132 | 133 | const res = await server.inject({ 134 | method: "PUT", 135 | url: serverConfig.routePrefix + "/users", 136 | payload: updateUser, 137 | headers: { authorization: login.token } 138 | }); 139 | 140 | var responseBody: IUser = JSON.parse(res.payload); 141 | assert.equal(200, res.statusCode); 142 | assert.equal("New Name", responseBody.name); 143 | }); 144 | }); 145 | -------------------------------------------------------------------------------- /test/utils.ts: -------------------------------------------------------------------------------- 1 | import * as Database from "../src/database"; 2 | 3 | export function createTaskDummy(userId?: string, name?: string, description?: string) { 4 | var user = { 5 | name: name || "dummy task", 6 | description: description || "I'm a dummy task!" 7 | }; 8 | 9 | if (userId) { 10 | user["userId"] = userId; 11 | } 12 | 13 | return user; 14 | } 15 | 16 | export function createUserDummy(email?: string) { 17 | var user = { 18 | email: email || "dummy@mail.com", 19 | name: "Dummy Jones", 20 | password: "123123" 21 | }; 22 | 23 | return user; 24 | } 25 | 26 | export function clearDatabase(database: Database.IDatabase, done: MochaDone) { 27 | var promiseUser = database.userModel.remove({}); 28 | var promiseTask = database.taskModel.remove({}); 29 | 30 | Promise.all([promiseUser, promiseTask]) 31 | .then(() => { 32 | done(); 33 | }) 34 | .catch(error => { 35 | console.log(error); 36 | }); 37 | } 38 | 39 | export function createSeedTaskData(database: Database.IDatabase, done: MochaDone) { 40 | return database.userModel 41 | .create(createUserDummy()) 42 | .then(user => { 43 | return Promise.all([ 44 | database.taskModel.create( 45 | createTaskDummy(user._id, "Task 1", "Some dummy data 1") 46 | ), 47 | database.taskModel.create( 48 | createTaskDummy(user._id, "Task 2", "Some dummy data 2") 49 | ), 50 | database.taskModel.create( 51 | createTaskDummy(user._id, "Task 3", "Some dummy data 3") 52 | ) 53 | ]); 54 | }) 55 | .then(task => { 56 | done(); 57 | }) 58 | .catch(error => { 59 | console.log(error); 60 | }); 61 | } 62 | 63 | export function createSeedUserData(database: Database.IDatabase, done: MochaDone) { 64 | database.userModel 65 | .create(createUserDummy()) 66 | .then(user => { 67 | done(); 68 | }) 69 | .catch(error => { 70 | console.log(error); 71 | }); 72 | } 73 | 74 | export async function login(server, config, user) { 75 | if (!user) { 76 | user = createUserDummy(); 77 | } 78 | 79 | return server.inject({ 80 | method: "POST", 81 | url: config.routePrefix + "/users/login", 82 | payload: { email: user.email, password: user.password } 83 | }); 84 | } 85 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "build", 4 | "target": "es6", 5 | "module": "commonjs", 6 | "moduleResolution": "node", 7 | "sourceMap": true, 8 | "typeRoots": ["node_modules/@types"] 9 | }, 10 | "include": [ 11 | "src/**/**.ts", 12 | "test/**/**.ts" 13 | ], 14 | "exclude": [ 15 | "node_modules" 16 | ] 17 | } -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "class-name": true, 4 | "curly": true, 5 | "eofline": false, 6 | "forin": true, 7 | "indent": [ 8 | true, 9 | 4 10 | ], 11 | "label-position": true, 12 | "max-line-length": [ 13 | true, 14 | 800 15 | ], 16 | "no-arg": true, 17 | "no-bitwise": true, 18 | "no-console": [ 19 | true, 20 | "debug", 21 | "info", 22 | "time", 23 | "timeEnd", 24 | "trace" 25 | ], 26 | "no-construct": true, 27 | "no-debugger": true, 28 | "no-duplicate-variable": true, 29 | "no-empty": false, 30 | "no-eval": true, 31 | "no-string-literal": false, 32 | "no-trailing-whitespace": true, 33 | "no-unused-variable": false, 34 | "one-line": [ 35 | true, 36 | "check-open-brace", 37 | "check-catch", 38 | "check-else", 39 | "check-whitespace" 40 | ], 41 | "radix": true, 42 | "semicolon": [ 43 | true, 44 | "always" 45 | ], 46 | "triple-equals": [ 47 | true, 48 | "allow-null-check" 49 | ], 50 | "variable-name": false, 51 | "whitespace": [ 52 | true, 53 | "check-branch", 54 | "check-decl", 55 | "check-operator", 56 | "check-separator" 57 | ] 58 | } 59 | } --------------------------------------------------------------------------------