├── .env.demo ├── .gitignore ├── .prettierrc ├── LICENSE ├── README.md ├── assets └── logo.png ├── migrate.ts ├── migrations └── 20170807000001-create-table-users.ts ├── package.json ├── src ├── app.module.ts ├── modules │ ├── auth │ │ ├── auth.controller.ts │ │ ├── auth.module.ts │ │ ├── auth.service.ts │ │ └── interfaces │ │ │ └── auth-service.interface.ts │ ├── database │ │ ├── database.module.ts │ │ └── database.provider.ts │ └── users │ │ ├── interfaces │ │ ├── index.ts │ │ ├── user-service.interface.ts │ │ └── user.interface.ts │ │ ├── user.controller.ts │ │ ├── user.entity.ts │ │ ├── user.module.ts │ │ ├── user.provider.ts │ │ └── user.service.ts ├── server.ts └── shared │ ├── config │ ├── database.ts │ ├── error-message.ts │ └── interfaces │ │ ├── data-base.interface.ts │ │ └── error-message.interface.ts │ ├── errors │ ├── index.ts │ └── message-code-error.ts │ ├── filters │ └── dispatch-error.ts │ ├── index.ts │ └── middlewares │ └── auth.middleware.ts ├── tsconfig.json └── tslint.json /.env.demo: -------------------------------------------------------------------------------- 1 | NODE_ENV=development 2 | DB_DIALECT=postgres 3 | DB_USER=database 4 | DB_PASSWORD=database 5 | DB_NAME=database 6 | DB_HOST=127.0.0.1 7 | DB_PORT=5432 8 | JWT_ID=jsonwebtoken 9 | JWT_KEY=secretKey -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | #env 2 | .env 3 | 4 | #js file and map 5 | src/**/*.js 6 | src/**/*.js.map 7 | tests/**/*.js 8 | tests/**/*.js.map 9 | migrations/**/*.js 10 | migrations/**/*.js.map 11 | migrate.js 12 | migrate.js.map 13 | 14 | # dependencies 15 | /node_modules 16 | 17 | # IDE 18 | /.idea 19 | /.awcache 20 | /.vscode 21 | 22 | # misc 23 | npm-debug.log 24 | 25 | # build 26 | /build 27 | 28 | # tests 29 | /coverage 30 | /.nyc_output 31 | 32 | #Package 33 | package-lock.json -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "typescript", 3 | "singleQuote": true, 4 | "tabWidth": 4, 5 | "semi": true, 6 | "bracketSpacing": true, 7 | "proseWrap": never, 8 | "printWidth": 120 9 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Adrien de Peretti 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Nest](assets/logo.png) 2 | 3 | ### Project made with [nest](https://github.com/kamilmysliwiec/nest/blob/master/Readme.md) and use 4 | 5 | - [Sequelize](https://github.com/sequelize/sequelize) 6 | - [JWT](https://jwt.io/) 7 | 8 | ### And what about this repo ? 9 | 10 | This project is a starter kit which implement the following : 11 | 12 | - Nest.js 13 | - Sequelize (ORM) 14 | - Umzug (Migration) 15 | - Dotenv (Evironement variable) 16 | - JWT (For Json Web Token authentication) 17 | 18 | ### How it works 19 | 20 | - To format code `npm run format` 21 | - Start the server `npm start` 22 | - To run up/down migration `npm run migrate {up/down}` 23 | 24 | ### Configuration 25 | 26 | To configure put all config file in the `./src/config/*`. 27 | To use the env variable, remove `.demo` from `.env.demo`. -------------------------------------------------------------------------------- /assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adrien2p/nestjs-sequelize-jwt/eb26b801162abbe9a00bf26f58dbe3a85ac4fc7c/assets/logo.png -------------------------------------------------------------------------------- /migrate.ts: -------------------------------------------------------------------------------- 1 | import * as dotenv from 'dotenv'; 2 | dotenv.config(); 3 | 4 | import * as path from 'path'; 5 | import * as childProcess from 'child_process'; 6 | import * as Promise from 'bluebird'; 7 | import { databaseConfig } from './src/shared/config/database'; 8 | import { Sequelize } from 'sequelize-typescript'; 9 | 10 | const Umzug = require('umzug'); 11 | const DB_NAME = process.env.DB_NAME; 12 | const DB_USER = process.env.DB_USER; 13 | 14 | let config; 15 | switch (process.env.NODE_ENV) { 16 | case 'prod': 17 | case 'production': 18 | config = databaseConfig.production; 19 | case 'dev': 20 | case 'development': 21 | config = databaseConfig.development; 22 | case 'test': 23 | config = databaseConfig.test; 24 | default: 25 | config = databaseConfig.development; 26 | } 27 | 28 | const sequelize = new Sequelize(config); 29 | 30 | const umzug = new Umzug({ 31 | storage: 'sequelize', 32 | storageOptions: { sequelize }, 33 | 34 | // see: https://github.com/sequelize/umzug/issues/17 35 | migrations: { 36 | params: [ 37 | sequelize, 38 | sequelize.constructor, // DataTypes 39 | function() { 40 | throw new Error( 41 | 'Migration tried to use old style "done" callback. Please upgrade to "umzug" and return a promise instead.' 42 | ); 43 | } 44 | ], 45 | path: './src/modules/common/migrations', 46 | pattern: /\.ts$/ 47 | }, 48 | 49 | logging: function() { 50 | console.log.apply(null, arguments); 51 | } 52 | }); 53 | 54 | function logUmzugEvent(eventName) { 55 | return function(name, migration) { 56 | console.log(`${name} ${eventName}`); 57 | }; 58 | } 59 | umzug.on('migrating', logUmzugEvent('migrating')); 60 | umzug.on('migrated', logUmzugEvent('migrated')); 61 | umzug.on('reverting', logUmzugEvent('reverting')); 62 | umzug.on('reverted', logUmzugEvent('reverted')); 63 | 64 | function cmdStatus() { 65 | let result: any = {}; 66 | 67 | return umzug 68 | .executed() 69 | .then(executed => { 70 | result.executed = executed; 71 | return umzug.pending(); 72 | }) 73 | .then(pending => { 74 | result.pending = pending; 75 | return result; 76 | }) 77 | .then(({ executed, pending }) => { 78 | executed = executed.map(m => { 79 | m.name = path.basename(m.file, '.ts'); 80 | return m; 81 | }); 82 | pending = pending.map(m => { 83 | m.name = path.basename(m.file, '.ts'); 84 | return m; 85 | }); 86 | 87 | const current = executed.length > 0 ? executed[0].file : ''; 88 | const status = { 89 | current: current, 90 | executed: executed.map(m => m.file), 91 | pending: pending.map(m => m.file) 92 | }; 93 | 94 | console.log(JSON.stringify(status, null, 2)); 95 | 96 | return { executed, pending }; 97 | }); 98 | } 99 | 100 | function cmdMigrate() { 101 | return umzug.up(); 102 | } 103 | 104 | function cmdMigrateNext() { 105 | return cmdStatus().then(({ executed, pending }) => { 106 | if (pending.length === 0) { 107 | return Promise.reject(new Error('No pending migrations')); 108 | } 109 | const next = pending[0].name; 110 | return umzug.up({ to: next }); 111 | }); 112 | } 113 | 114 | function cmdReset() { 115 | return umzug.down({ to: 0 }); 116 | } 117 | 118 | function cmdResetPrev() { 119 | return cmdStatus().then(({ executed, pending }) => { 120 | if (executed.length === 0) { 121 | return Promise.reject(new Error('Already at initial state')); 122 | } 123 | const prev = executed[executed.length - 1].name; 124 | return umzug.down({ to: prev }); 125 | }); 126 | } 127 | 128 | function cmdHardReset() { 129 | return new Promise((resolve, reject) => { 130 | setImmediate(() => { 131 | try { 132 | console.log(`dropdb ${DB_NAME}`); 133 | childProcess.spawnSync(`dropdb ${DB_NAME}`); 134 | console.log(`createdb ${DB_NAME} --username ${DB_USER}`); 135 | childProcess.spawnSync(`createdb ${DB_NAME} --username ${DB_USER}`); 136 | resolve(); 137 | } catch (e) { 138 | console.log(e); 139 | reject(e); 140 | } 141 | }); 142 | }); 143 | } 144 | 145 | const cmd = process.argv[2].trim(); 146 | let executedCmd; 147 | 148 | console.log(`${cmd.toUpperCase()} BEGIN`); 149 | switch (cmd) { 150 | case 'status': 151 | executedCmd = cmdStatus(); 152 | break; 153 | 154 | case 'up': 155 | case 'migrate': 156 | executedCmd = cmdMigrate(); 157 | break; 158 | 159 | case 'next': 160 | case 'migrate-next': 161 | executedCmd = cmdMigrateNext(); 162 | break; 163 | 164 | case 'down': 165 | case 'reset': 166 | executedCmd = cmdReset(); 167 | break; 168 | 169 | case 'prev': 170 | case 'reset-prev': 171 | executedCmd = cmdResetPrev(); 172 | break; 173 | 174 | case 'reset-hard': 175 | executedCmd = cmdHardReset(); 176 | break; 177 | 178 | default: 179 | console.log(`invalid cmd: ${cmd}`); 180 | process.exit(1); 181 | } 182 | 183 | executedCmd 184 | .then(result => { 185 | const doneStr = `${cmd.toUpperCase()} DONE`; 186 | console.log(doneStr); 187 | console.log('=============================================================================='); 188 | }) 189 | .catch(err => { 190 | const errorStr = `${cmd.toUpperCase()} ERROR`; 191 | console.log(errorStr); 192 | console.log('=============================================================================='); 193 | console.log(err); 194 | console.log('=============================================================================='); 195 | }) 196 | .then(() => { 197 | if (cmd !== 'status' && cmd !== 'reset-hard') { 198 | return cmdStatus(); 199 | } 200 | return Promise.resolve(); 201 | }) 202 | .then(() => process.exit(0)); 203 | -------------------------------------------------------------------------------- /migrations/20170807000001-create-table-users.ts: -------------------------------------------------------------------------------- 1 | export async function up(sequelize) { 2 | // language=PostgreSQL 3 | sequelize.query(` 4 | CREATE TABLE "users" ( 5 | "id" SERIAL UNIQUE PRIMARY KEY NOT NULL, 6 | "firstName" VARCHAR(30) NOT NULL, 7 | "lastName" VARCHAR(30) NOT NULL, 8 | "email" VARCHAR(100) UNIQUE NOT NULL, 9 | "password" TEXT NOT NULL, 10 | "birthday" TIMESTAMP, 11 | "createdAt" TIMESTAMP NOT NULL, 12 | "updatedAt" TIMESTAMP NOT NULL, 13 | "deletedAt" TIMESTAMP 14 | ); 15 | `); 16 | 17 | console.log('*Table users created!*'); 18 | } 19 | 20 | export async function down(sequelize) { 21 | // language=PostgreSQL 22 | sequelize.query(`DROP TABLE users`); 23 | } 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nestjs-sequelize-jwt", 3 | "version": "5.0.2", 4 | "description": "Nest + Sequelize + jwt", 5 | "main": "src/server.ts", 6 | "keywords": [ 7 | "nest", 8 | "nest-js", 9 | "nestjs", 10 | "sequelize", 11 | "orm", 12 | "nodejs", 13 | "node", 14 | "typescript", 15 | "jwt", 16 | "jsonwebtoken", 17 | "dotenv" 18 | ], 19 | "author": { 20 | "name": "Adrien de Peretti", 21 | "email": "adrien.deperetti.freelance@gmail.com" 22 | }, 23 | "license": "MIT", 24 | "bugs": { 25 | "url": "https://github.com/adrien2p/nest-js-sequelize-jwt/issues" 26 | }, 27 | "scripts": { 28 | "format": "prettier --write --parser typescript \"**/*.ts\"", 29 | "migrate": "ts-node migrate.ts", 30 | "start": "ts-node src/server.ts --env=NODE_ENV", 31 | "test": "jest", 32 | "test:cov": "jest --coverage", 33 | "test:e2e": "jest --config ./test/jest-e2e.json" 34 | }, 35 | "dependencies": { 36 | "@nestjs/common": "^6.0.2", 37 | "@nestjs/core": "^6.0.2", 38 | "@nestjs/platform-express": "^6.0.4", 39 | "@types/dotenv": "^4.0.3", 40 | "@types/express": "^4.11.1", 41 | "@types/jsonwebtoken": "^7.2.3", 42 | "@types/lodash": "^4.14.76", 43 | "@types/reflect-metadata": "0.0.5", 44 | "@types/sequelize": "^4.0.75", 45 | "body-parser": "^1.18.2", 46 | "dotenv": "^4.0.0", 47 | "express": "^4.16.3", 48 | "jsonwebtoken": "^7.4.3", 49 | "pg": "^7.3.0", 50 | "pg-hstore": "^2.3.2", 51 | "prettier": "^1.13.7", 52 | "reflect-metadata": "^0.1.10", 53 | "rxjs": "^5.4.3", 54 | "sequelize": "^4.13.0", 55 | "sequelize-typescript": "^0.6.5", 56 | "ts-jest": "^22.4.5", 57 | "tslint-config-prettier": "^1.13.0", 58 | "umzug": "^2.0.1" 59 | }, 60 | "devDependencies": { 61 | "@nestjs/testing": "^6.0.0", 62 | "@types/jest": "^22.2.3", 63 | "@types/node": "^8.0.31", 64 | "jest": "^22.4.3", 65 | "ts-node": "^3.3.0", 66 | "tslint": "^5.7.0", 67 | "tslint-config-standard": "^6.0.1", 68 | "typescript": "^2.5.3" 69 | }, 70 | "jest": { 71 | "moduleFileExtensions": [ 72 | "js", 73 | "json", 74 | "ts" 75 | ], 76 | "rootDir": "src", 77 | "testRegex": ".spec.ts$", 78 | "transform": { 79 | "^.+\\.(t|j)s$": "ts-jest" 80 | }, 81 | "coverageDirectory": "../coverage" 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { UserModule } from './modules/users/user.module'; 3 | import { AuthModule } from './modules/auth/auth.module'; 4 | 5 | @Module({ 6 | imports: [UserModule, AuthModule] 7 | }) 8 | export class ApplicationModule {} 9 | -------------------------------------------------------------------------------- /src/modules/auth/auth.controller.ts: -------------------------------------------------------------------------------- 1 | import { Body, Controller, HttpStatus, Post, Res } from '@nestjs/common'; 2 | import { MessageCodeError } from '../../shared/index'; 3 | import { AuthService } from './auth.service'; 4 | 5 | @Controller() 6 | export class AuthController { 7 | constructor(private authService: AuthService) {} 8 | 9 | @Post('login') 10 | public async login(@Body() body, @Res() res) { 11 | if (!body) throw new MessageCodeError('auth:login:missingInformation'); 12 | if (!body.email) throw new MessageCodeError('auth:login:missingEmail'); 13 | if (!body.password) throw new MessageCodeError('auth:login:missingPassword'); 14 | 15 | const token = await this.authService.sign(body); 16 | res.status(HttpStatus.ACCEPTED).json('Bearer ' + token); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/modules/auth/auth.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { AuthService } from './auth.service'; 3 | import { AuthController } from './auth.controller'; 4 | 5 | @Module({ 6 | controllers: [AuthController], 7 | providers: [AuthService] 8 | }) 9 | export class AuthModule {} 10 | -------------------------------------------------------------------------------- /src/modules/auth/auth.service.ts: -------------------------------------------------------------------------------- 1 | import * as jwt from 'jsonwebtoken'; 2 | import * as crypto from 'crypto'; 3 | import { MessageCodeError } from '../../shared/index'; 4 | import { IAuthService, IJwtOptions } from './interfaces/auth-service.interface'; 5 | import { User } from '../users/user.entity'; 6 | import { Injectable } from '@nestjs/common'; 7 | 8 | @Injectable() 9 | export class AuthService implements IAuthService { 10 | private _options: IJwtOptions = { 11 | algorithm: 'HS256', 12 | expiresIn: '2 days', 13 | jwtid: process.env.JWT_ID || '' 14 | }; 15 | 16 | get options(): IJwtOptions { 17 | return this._options; 18 | } 19 | 20 | set options(value: IJwtOptions) { 21 | this._options.algorithm = value.algorithm; 22 | } 23 | 24 | public async sign(credentials: { email: string; password: string }): Promise { 25 | const user = await User.findOne({ 26 | where: { 27 | email: credentials.email, 28 | password: crypto.createHmac('sha256', credentials.password).digest('hex') 29 | } 30 | }); 31 | if (!user) throw new MessageCodeError('user:notFound'); 32 | 33 | const payload = { 34 | id: user.id, 35 | email: user.email 36 | }; 37 | 38 | return await jwt.sign(payload, process.env.JWT_KEY || '', this._options); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/modules/auth/interfaces/auth-service.interface.ts: -------------------------------------------------------------------------------- 1 | export interface IAuthService { 2 | options: IJwtOptions; 3 | 4 | /** 5 | * @description: Sign the user, create a new token before it insert in the response header Authorization. 6 | * @param {email: string; password: string} credentials 7 | * @return {Promise} 8 | */ 9 | sign(credentials: { email: string; password: string }): Promise; 10 | } 11 | 12 | export interface IJwtOptions { 13 | algorithm: string; 14 | expiresIn: number | string; 15 | jwtid: string; 16 | } 17 | -------------------------------------------------------------------------------- /src/modules/database/database.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { databaseProvider } from './database.provider'; 3 | 4 | @Module({ 5 | providers: [databaseProvider], 6 | exports: [databaseProvider] 7 | }) 8 | export class DatabaseModule {} 9 | -------------------------------------------------------------------------------- /src/modules/database/database.provider.ts: -------------------------------------------------------------------------------- 1 | import { Sequelize } from 'sequelize-typescript'; 2 | import { databaseConfig } from '../../shared/index'; 3 | import { User } from '../users/user.entity'; 4 | 5 | export const databaseProvider = { 6 | provide: 'SequelizeInstance', 7 | useFactory: () => { 8 | let config; 9 | switch (process.env.NODE_ENV) { 10 | case 'prod': 11 | case 'production': 12 | config = databaseConfig.production; 13 | case 'dev': 14 | case 'development': 15 | config = databaseConfig.development; 16 | default: 17 | config = databaseConfig.development; 18 | } 19 | 20 | const sequelize = new Sequelize(config); 21 | sequelize.addModels([User]); 22 | /* await sequelize.sync(); add this if you want to sync model and DB.*/ 23 | return sequelize; 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /src/modules/users/interfaces/index.ts: -------------------------------------------------------------------------------- 1 | export * from './user.interface'; 2 | export * from './user-service.interface'; 3 | -------------------------------------------------------------------------------- /src/modules/users/interfaces/user-service.interface.ts: -------------------------------------------------------------------------------- 1 | import { User } from '../user.entity'; 2 | import { IUser } from '../interfaces/index'; 3 | 4 | export interface IUserService { 5 | findAll(): Promise>; 6 | findById(id: number): Promise; 7 | findOne(options: Object): Promise; 8 | create(user: IUser): Promise; 9 | update(id: number, newValue: IUser): Promise; 10 | delete(id: number): Promise; 11 | } 12 | -------------------------------------------------------------------------------- /src/modules/users/interfaces/user.interface.ts: -------------------------------------------------------------------------------- 1 | export interface IUser { 2 | firstName: string; 3 | lastName: string; 4 | email: string; 5 | password: string; 6 | birthday?: Date; 7 | } 8 | -------------------------------------------------------------------------------- /src/modules/users/user.controller.ts: -------------------------------------------------------------------------------- 1 | import { Body, Controller, Delete, Get, HttpStatus, Param, Post, Put, Res } from '@nestjs/common'; 2 | import { MessageCodeError } from '../../shared/index'; 3 | import { UserService } from './user.service'; 4 | 5 | @Controller('users') 6 | export class UserController { 7 | constructor(private readonly usersService: UserService) {} 8 | 9 | @Get() 10 | public async index(@Res() res) { 11 | const users = await this.usersService.findAll(); 12 | return res.status(HttpStatus.OK).json(users); 13 | } 14 | 15 | @Post() 16 | public async create(@Body() body, @Res() res) { 17 | if (!body || (body && Object.keys(body).length === 0)) 18 | throw new MessageCodeError('user:create:missingInformation'); 19 | 20 | await this.usersService.create(body); 21 | return res.status(HttpStatus.CREATED).send(); 22 | } 23 | 24 | @Get(':id') 25 | public async show(@Param('id') id: number, @Res() res) { 26 | if (!id) throw new MessageCodeError('user:show:missingId'); 27 | 28 | const user = await this.usersService.findById(id); 29 | return res.status(HttpStatus.OK).json(user); 30 | } 31 | 32 | @Put(':id') 33 | public async update(@Body() body, @Param('id') id: number, @Res() res) { 34 | if (!id) throw new MessageCodeError('user:update:missingId'); 35 | 36 | await this.usersService.update(id, body); 37 | return res.status(HttpStatus.OK).send(); 38 | } 39 | 40 | @Delete(':id') 41 | public async delete(@Param('id') id: number, @Res() res) { 42 | if (!id) throw new MessageCodeError('user:delete:missingId'); 43 | 44 | await this.usersService.delete(id); 45 | return res.status(HttpStatus.OK).send(); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/modules/users/user.entity.ts: -------------------------------------------------------------------------------- 1 | import * as crypto from 'crypto'; 2 | import { 3 | Table, 4 | Column, 5 | Model, 6 | DataType, 7 | CreatedAt, 8 | UpdatedAt, 9 | DeletedAt, 10 | BeforeValidate, 11 | BeforeCreate 12 | } from 'sequelize-typescript'; 13 | import { IDefineOptions } from 'sequelize-typescript/lib/interfaces/IDefineOptions'; 14 | import { MessageCodeError } from '../../shared/errors/message-code-error'; 15 | 16 | const tableOptions: IDefineOptions = { 17 | timestamp: true, 18 | tableName: 'users' 19 | } as IDefineOptions; 20 | 21 | @Table(tableOptions) 22 | export class User extends Model { 23 | @Column({ 24 | type: DataType.NUMERIC, 25 | allowNull: false, 26 | autoIncrement: true, 27 | unique: true, 28 | primaryKey: true 29 | }) 30 | public id: number; 31 | 32 | @Column({ 33 | type: DataType.CHAR(30), 34 | allowNull: false 35 | }) 36 | public firstName: string; 37 | 38 | @Column({ 39 | type: DataType.CHAR(30), 40 | allowNull: false 41 | }) 42 | public lastName: string; 43 | 44 | @Column({ 45 | type: DataType.CHAR(100), 46 | allowNull: false, 47 | validate: { 48 | isEmail: true, 49 | isUnique: async (value: string, next: Function): Promise => { 50 | const isExist = await User.findOne({ where: { email: value } }); 51 | if (isExist) { 52 | const error = new MessageCodeError('user:create:emailAlreadyExist'); 53 | next(error); 54 | } 55 | next(); 56 | } 57 | } 58 | }) 59 | public email: string; 60 | 61 | @Column({ 62 | type: DataType.TEXT, 63 | allowNull: false 64 | }) 65 | public password: string; 66 | 67 | @Column({ type: DataType.DATE }) 68 | public birthday: Date; 69 | 70 | @CreatedAt public createdAt: Date; 71 | 72 | @UpdatedAt public updatedAt: Date; 73 | 74 | @DeletedAt public deletedAt: Date; 75 | 76 | @BeforeValidate 77 | public static validateData(user: User, options: any) { 78 | if (!options.transaction) throw new Error('Missing transaction.'); 79 | if (!user.firstName) throw new MessageCodeError('user:create:missingFirstName'); 80 | if (!user.lastName) throw new MessageCodeError('user:create:missingLastName'); 81 | if (!user.email) throw new MessageCodeError('user:create:missingEmail'); 82 | if (!user.password) throw new MessageCodeError('user:create:missingPassword'); 83 | } 84 | 85 | @BeforeCreate 86 | public static async hashPassword(user: User, options: any) { 87 | if (!options.transaction) throw new Error('Missing transaction.'); 88 | 89 | user.password = crypto.createHmac('sha256', user.password).digest('hex'); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/modules/users/user.module.ts: -------------------------------------------------------------------------------- 1 | import { Module, RequestMethod } from '@nestjs/common'; 2 | import { DatabaseModule } from '../database/database.module'; 3 | import { MiddlewareConsumer } from '@nestjs/common/interfaces/middleware'; 4 | import { AuthMiddleware } from '../../shared/index'; 5 | import { UserController } from './user.controller'; 6 | import { UserService } from './user.service'; 7 | import { usersProvider } from './user.provider'; 8 | 9 | @Module({ 10 | imports: [DatabaseModule], 11 | controllers: [UserController], 12 | providers: [UserService, usersProvider] 13 | }) 14 | export class UserModule { 15 | public configure(consumer: MiddlewareConsumer) { 16 | consumer 17 | .apply(AuthMiddleware) 18 | .forRoutes( 19 | { path: '/users', method: RequestMethod.GET }, 20 | { path: '/users/:id', method: RequestMethod.GET }, 21 | { path: '/users/:id', method: RequestMethod.PUT }, 22 | { path: '/users/:id', method: RequestMethod.DELETE } 23 | ); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/modules/users/user.provider.ts: -------------------------------------------------------------------------------- 1 | import { User } from './user.entity'; 2 | 3 | export const usersProvider = { 4 | provide: 'UserRepository', 5 | useValue: User 6 | }; 7 | -------------------------------------------------------------------------------- /src/modules/users/user.service.ts: -------------------------------------------------------------------------------- 1 | import { Inject, Injectable } from '@nestjs/common'; 2 | import { MessageCodeError } from '../../shared/errors/message-code-error'; 3 | import { IUser, IUserService } from './interfaces/index'; 4 | import { User } from './user.entity'; 5 | 6 | @Injectable() 7 | export class UserService implements IUserService { 8 | constructor( 9 | @Inject('UserRepository') private readonly userRepository: typeof User, 10 | @Inject('SequelizeInstance') private readonly sequelizeInstance 11 | ) {} 12 | 13 | public async findAll(): Promise> { 14 | return await this.userRepository.findAll(); 15 | } 16 | 17 | public async findOne(options: Object): Promise { 18 | return await this.userRepository.findOne(options); 19 | } 20 | 21 | public async findById(id: number): Promise { 22 | return await this.userRepository.findById(id); 23 | } 24 | 25 | public async create(user: IUser): Promise { 26 | return await this.sequelizeInstance.transaction(async transaction => { 27 | return await this.userRepository.create(user, { 28 | returning: true, 29 | transaction 30 | }); 31 | }); 32 | } 33 | 34 | public async update(id: number, newValue: IUser): Promise { 35 | return await this.sequelizeInstance.transaction(async transaction => { 36 | let user = await this.userRepository.findById(id, { 37 | transaction 38 | }); 39 | if (!user) throw new MessageCodeError('user:notFound'); 40 | 41 | user = this._assign(user, newValue); 42 | return await user.save({ 43 | returning: true, 44 | transaction 45 | }); 46 | }); 47 | } 48 | 49 | public async delete(id: number): Promise { 50 | return await this.sequelizeInstance.transaction(async transaction => { 51 | return await this.userRepository.destroy({ 52 | where: { id }, 53 | transaction 54 | }); 55 | }); 56 | } 57 | 58 | /** 59 | * @description: Assign new value in the user found in the database. 60 | * 61 | * @param {IUser} user 62 | * @param {IUser} newValue 63 | * @return {User} 64 | * @private 65 | */ 66 | private _assign(user: IUser, newValue: IUser): User { 67 | for (const key of Object.keys(user)) { 68 | if (user[key] !== newValue[key]) user[key] = newValue[key]; 69 | } 70 | 71 | return user as User; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/server.ts: -------------------------------------------------------------------------------- 1 | import * as dotenv from 'dotenv'; 2 | dotenv.config(); 3 | 4 | import { NestFactory } from '@nestjs/core'; 5 | import { DispatchError } from './shared/filters/dispatch-error'; 6 | import { ApplicationModule } from './app.module'; 7 | 8 | async function bootstrap(): Promise { 9 | const app = await NestFactory.create(ApplicationModule); 10 | app.useGlobalFilters(new DispatchError()); 11 | await app.listen(3000); 12 | } 13 | 14 | bootstrap(); 15 | -------------------------------------------------------------------------------- /src/shared/config/database.ts: -------------------------------------------------------------------------------- 1 | import { IDatabaseConfig } from './interfaces/data-base.interface'; 2 | 3 | export const databaseConfig: IDatabaseConfig = { 4 | development: { 5 | username: process.env.DB_USER || '', 6 | password: process.env.DB_PASSWORD || '', 7 | database: process.env.DB_NAME || '', 8 | host: process.env.DB_HOST || '127.0.0.1', 9 | port: Number(process.env.DB_PORT) || 5432, 10 | dialect: 'postgres', 11 | logging: false, 12 | force: true, 13 | timezone: '+02:00' 14 | }, 15 | production: { 16 | username: process.env.DB_USER || '', 17 | password: process.env.DB_PASSWORD || '', 18 | database: process.env.DB_NAME || '', 19 | host: process.env.DB_HOST || '127.0.0.1', 20 | port: Number(process.env.DB_PORT) || 5432, 21 | dialect: 'postgres', 22 | logging: false, 23 | force: true, 24 | timezone: '+02:00' 25 | }, 26 | test: { 27 | username: process.env.DB_USER || '', 28 | password: process.env.DB_PASSWORD || '', 29 | database: process.env.DB_NAME || '', 30 | host: process.env.DB_HOST || '127.0.0.1', 31 | port: Number(process.env.DB_PORT) || 5432, 32 | dialect: 'postgres', 33 | logging: true, 34 | force: true, 35 | timezone: '+02:00' 36 | } 37 | }; 38 | -------------------------------------------------------------------------------- /src/shared/config/error-message.ts: -------------------------------------------------------------------------------- 1 | import { HttpStatus } from '@nestjs/common'; 2 | import { IErrorMessages } from './interfaces/error-message.interface'; 3 | 4 | export const errorMessagesConfig: { [messageCode: string]: IErrorMessages } = { 5 | 'user:create:missingInformation': { 6 | type: 'BadRequest', 7 | httpStatus: HttpStatus.BAD_REQUEST, 8 | errorMessage: 'Unable to create a new user with missing information.', 9 | userMessage: 'Impossible de créer un utilisateur avec des données manquantes.' 10 | }, 11 | 'user:create:missingFirstName': { 12 | type: 'BadRequest', 13 | httpStatus: HttpStatus.BAD_REQUEST, 14 | errorMessage: 'Unable to create a new user without first name.', 15 | userMessage: 'Veuillez indiquer votre prénom.' 16 | }, 17 | 'user:create:missingLastName': { 18 | type: 'BadRequest', 19 | httpStatus: HttpStatus.BAD_REQUEST, 20 | errorMessage: 'Unable to create a new user without last name.', 21 | userMessage: 'Veuillez indiquer votre nom.' 22 | }, 23 | 'user:create:missingEmail': { 24 | type: 'BadRequest', 25 | httpStatus: HttpStatus.BAD_REQUEST, 26 | errorMessage: 'Unable to create a new user without email.', 27 | userMessage: 'Veuillez indiquer votre adresse e-mail.' 28 | }, 29 | 'user:create:missingPassword': { 30 | type: 'BadRequest', 31 | httpStatus: HttpStatus.BAD_REQUEST, 32 | errorMessage: 'Unable to create a new user without password.', 33 | userMessage: 'Veuillez indiquer votre mot de passe.' 34 | }, 35 | 'user:create:emailAlreadyExist': { 36 | type: 'BadRequest', 37 | httpStatus: HttpStatus.BAD_REQUEST, 38 | errorMessage: 'Unable to create a new user with this email.', 39 | userMessage: "L'adresse e-mail que vous avez fourni est déjà utilisé." 40 | }, 41 | 'user:show:missingId': { 42 | type: 'BadRequest', 43 | httpStatus: HttpStatus.BAD_REQUEST, 44 | errorMessage: 'Unable to find the user caused by missing information.', 45 | userMessage: "Impossible de trouver un utilisateur sans fournir d'id." 46 | }, 47 | 'user:update:missingInformation': { 48 | type: 'BadRequest', 49 | httpStatus: HttpStatus.BAD_REQUEST, 50 | errorMessage: 'Unable to update the user caused by missing information.', 51 | userMessage: "Impossible de mettre à jour l'utilisateur avec des données manquantes." 52 | }, 53 | 'user:update:missingId': { 54 | type: 'BadRequest', 55 | httpStatus: HttpStatus.BAD_REQUEST, 56 | errorMessage: 'Unable to update the user caused by missing information.', 57 | userMessage: "Impossible de mettre à jour l'utilisateur avec des données manquantes." 58 | }, 59 | 'user:delete:missingId': { 60 | type: 'BadRequest', 61 | httpStatus: HttpStatus.BAD_REQUEST, 62 | errorMessage: 'Unable to delete the user caused by missing information.', 63 | userMessage: "Impossible de supprimer un utilisateur sans fournir d'id." 64 | }, 65 | 'user:notFound': { 66 | type: 'notFound', 67 | httpStatus: HttpStatus.NOT_FOUND, 68 | errorMessage: 'Unable to found the user with the provided information.', 69 | userMessage: 'Aucun utilisateur trouvé avec les informations fourni.' 70 | }, 71 | 'request:unauthorized': { 72 | type: 'unauthorized', 73 | httpStatus: HttpStatus.UNAUTHORIZED, 74 | errorMessage: 'Access unauthorized.', 75 | userMessage: 'Accès non autorisé.' 76 | }, 77 | 'auth:login:missingEmail': { 78 | type: 'BadRequest', 79 | httpStatus: HttpStatus.BAD_REQUEST, 80 | errorMessage: 'Unable to connect the user without email.', 81 | userMessage: 'Veuillez indiquer votre adresse e-mail.' 82 | }, 83 | 'auth:login:missingPassword': { 84 | type: 'BadRequest', 85 | httpStatus: HttpStatus.BAD_REQUEST, 86 | errorMessage: 'Unable to connect the user without password.', 87 | userMessage: 'Veuillez indiquer votre mot de passe.' 88 | } 89 | }; 90 | -------------------------------------------------------------------------------- /src/shared/config/interfaces/data-base.interface.ts: -------------------------------------------------------------------------------- 1 | export interface IDatabaseConfigAttributes { 2 | username: string; 3 | password: string; 4 | database: string; 5 | host: string; 6 | port: number; 7 | dialect: string; 8 | logging: boolean | Function; 9 | force: boolean; 10 | timezone: string; 11 | } 12 | 13 | export interface IDatabaseConfig { 14 | development: IDatabaseConfigAttributes; 15 | production: IDatabaseConfigAttributes; 16 | test: IDatabaseConfigAttributes; 17 | } 18 | -------------------------------------------------------------------------------- /src/shared/config/interfaces/error-message.interface.ts: -------------------------------------------------------------------------------- 1 | import { HttpStatus } from '@nestjs/common'; 2 | 3 | export interface IErrorMessages { 4 | type: string; 5 | httpStatus: HttpStatus; 6 | errorMessage: string; 7 | userMessage: string; 8 | } 9 | -------------------------------------------------------------------------------- /src/shared/errors/index.ts: -------------------------------------------------------------------------------- 1 | export * from './message-code-error'; 2 | -------------------------------------------------------------------------------- /src/shared/errors/message-code-error.ts: -------------------------------------------------------------------------------- 1 | import { errorMessagesConfig } from '../config/error-message'; 2 | import { IErrorMessages } from '../config/interfaces/error-message.interface'; 3 | 4 | export class MessageCodeError extends Error { 5 | public messageCode: string; 6 | public httpStatus: number; 7 | public errorMessage: string; 8 | 9 | constructor(messageCode: string) { 10 | super(); 11 | 12 | const errorMessageConfig = this.getMessageFromMessageCode(messageCode); 13 | if (!errorMessageConfig) throw new Error('Unable to find message code error.'); 14 | 15 | Error.captureStackTrace(this, this.constructor); 16 | this.name = this.constructor.name; 17 | this.httpStatus = errorMessageConfig.httpStatus; 18 | this.messageCode = messageCode; 19 | this.errorMessage = errorMessageConfig.errorMessage; 20 | this.message = errorMessageConfig.userMessage; 21 | } 22 | 23 | /** 24 | * @description: Find the error config by the given message code. 25 | * @param {string} messageCode 26 | * @return {IErrorMessages} 27 | */ 28 | private getMessageFromMessageCode(messageCode: string): IErrorMessages { 29 | let errorMessageConfig: IErrorMessages | undefined; 30 | Object.keys(errorMessagesConfig).some(key => { 31 | if (key === messageCode) { 32 | errorMessageConfig = errorMessagesConfig[key]; 33 | return true; 34 | } 35 | return false; 36 | }); 37 | 38 | if (!errorMessageConfig) throw new Error('Unable to find the given message code error.'); 39 | return errorMessageConfig; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/shared/filters/dispatch-error.ts: -------------------------------------------------------------------------------- 1 | import { ArgumentsHost, Catch, ExceptionFilter, HttpException, HttpStatus } from '@nestjs/common'; 2 | import { MessageCodeError } from '../errors/message-code-error'; 3 | import { ValidationError } from 'sequelize'; 4 | 5 | @Catch(MessageCodeError, ValidationError, HttpException, Error) 6 | export class DispatchError implements ExceptionFilter { 7 | public catch(err: any, host: ArgumentsHost) { 8 | const res = host.switchToHttp().getResponse(); 9 | 10 | if (err instanceof MessageCodeError) { 11 | /* MessageCodeError, Set all header variable to have a context for the client in case of MessageCodeError. */ 12 | res.setHeader('x-message-code-error', err.messageCode); 13 | res.setHeader('x-message', err.message); 14 | res.setHeader('x-httpStatus-error', err.httpStatus); 15 | 16 | return res.status(err.httpStatus).send(); 17 | } else if (err instanceof ValidationError) { 18 | /* Sequelize validation error. */ 19 | res.setHeader('x-message-code-error', (err as ValidationError).errors[0].type); 20 | res.setHeader('x-message', (err as ValidationError).errors[0].message); 21 | res.setHeader('x-httpStatus-error', HttpStatus.BAD_REQUEST); 22 | 23 | return res.status(HttpStatus.BAD_REQUEST).send(); 24 | } else { 25 | return res.status(HttpStatus.INTERNAL_SERVER_ERROR).send(); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/shared/index.ts: -------------------------------------------------------------------------------- 1 | export * from './config/database'; 2 | export * from './config/error-message'; 3 | export * from './filters/dispatch-error'; 4 | export * from './errors/index'; 5 | export * from './middlewares/auth.middleware'; 6 | -------------------------------------------------------------------------------- /src/shared/middlewares/auth.middleware.ts: -------------------------------------------------------------------------------- 1 | import * as jwt from 'jsonwebtoken'; 2 | import { Injectable, NestMiddleware } from '@nestjs/common'; 3 | import { MessageCodeError } from '../errors/message-code-error'; 4 | import { User } from '../../modules/users/user.entity'; 5 | 6 | @Injectable() 7 | export class AuthMiddleware implements NestMiddleware { 8 | public async use(req, res, next) { 9 | if (req.headers.authorization && (req.headers.authorization as string).split(' ')[0] === 'Bearer') { 10 | const token = (req.headers.authorization as string).split(' ')[1]; 11 | const decoded: any = jwt.verify(token, process.env.JWT_KEY || ''); 12 | const user = await User.findOne({ 13 | where: { 14 | id: decoded.id, 15 | email: decoded.email 16 | } 17 | }); 18 | if (!user) throw new MessageCodeError('request:unauthorized'); 19 | next(); 20 | } else { 21 | throw new MessageCodeError('request:unauthorized'); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": false, 5 | "noImplicitAny": false, 6 | "removeComments": true, 7 | "noLib": false, 8 | "allowSyntheticDefaultImports": true, 9 | "emitDecoratorMetadata": true, 10 | "experimentalDecorators": true, 11 | "target": "es6", 12 | "sourceMap": true, 13 | "allowJs": true, 14 | "outDir": "./dist", 15 | "baseUrl": "./src" 16 | }, 17 | "include": [ 18 | "migrate.ts", 19 | "src/server.ts", 20 | "src/shared/config/**/*.ts", 21 | "src/shared/filters/**/*.ts", 22 | "src/shared/errors/**/*.ts", 23 | "src/shared/middlewares/**/*.ts", 24 | "migrations/**/*.ts", 25 | "src/modules/common/models/**/*.ts", 26 | "src/modules/**/*.ts", 27 | "tests/**/*.ts" 28 | ], 29 | "exclude": [ 30 | "node_modules", 31 | "assets" 32 | ] 33 | } -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": [], 4 | "jsRules": { 5 | "no-unused-expression": true 6 | }, 7 | "rules": { 8 | "eofline": false, 9 | "quotemark": [ 10 | true, 11 | "single" 12 | ], 13 | "indent": false, 14 | "member-access": [ 15 | false 16 | ], 17 | "ordered-imports": [ 18 | false 19 | ], 20 | "max-line-length": [ 21 | true, 22 | 150 23 | ], 24 | "member-ordering": [ 25 | false 26 | ], 27 | "curly": false, 28 | "interface-name": [ 29 | false 30 | ], 31 | "array-type": [ 32 | false 33 | ], 34 | "no-empty-interface": false, 35 | "no-empty": false, 36 | "arrow-parens": false, 37 | "object-literal-sort-keys": false, 38 | "no-unused-expression": false, 39 | "max-classes-per-file": [ 40 | false 41 | ], 42 | "variable-name": [ 43 | false 44 | ], 45 | "one-line": [ 46 | false 47 | ], 48 | "one-variable-per-declaration": [ 49 | false 50 | ] 51 | }, 52 | "rulesDirectory": [] 53 | } --------------------------------------------------------------------------------