├── .env.demo ├── .gitignore ├── LICENSE ├── README.md ├── assets ├── fulfillment.png └── logo.png ├── migrate.ts ├── package.json ├── src ├── modules │ ├── api.ai │ │ ├── actions │ │ │ ├── abstractAction.ts │ │ │ └── jeeves.service.ts │ │ ├── apiAi.controller.ts │ │ ├── apiAi.module.ts │ │ └── interfaces │ │ │ ├── IAbstractAction.ts │ │ │ ├── IActionService.ts │ │ │ ├── IActionServiceResponse.ts │ │ │ ├── IInterceptorService.ts │ │ │ ├── IJeevesService.ts │ │ │ └── IResponse.ts │ ├── app.module.ts │ ├── auth │ │ ├── auth.controller.ts │ │ ├── auth.module.ts │ │ ├── auth.service.ts │ │ ├── interfaces │ │ │ └── IAuthService.ts │ │ └── tests │ │ │ ├── auth.service.test.ts │ │ │ └── fixtures │ │ │ └── fake.data.ts │ ├── common │ │ ├── config │ │ │ ├── database.ts │ │ │ ├── errorMessages.ts │ │ │ ├── global.ts │ │ │ ├── index.ts │ │ │ └── interfaces │ │ │ │ ├── IDatabase.ts │ │ │ │ └── IErrorMessages.ts │ │ ├── filters │ │ │ └── DispatchError.ts │ │ ├── index.ts │ │ ├── lib │ │ │ ├── error │ │ │ │ └── MessageCodeError.ts │ │ │ └── index.ts │ │ ├── middlewares │ │ │ ├── apiAi.jeeves.checkOrCreateUser.middleware.ts │ │ │ ├── apiAi.jeeves.middleware.ts │ │ │ ├── auth.middleware.ts │ │ │ └── index.ts │ │ ├── migrations │ │ │ ├── 20170807000001-create-table-actions.ts │ │ │ ├── 20170807000001-create-table-users.ts │ │ │ └── 20170828000001-create-table-apiAiUsers.ts │ │ └── models │ │ │ ├── Action.ts │ │ │ ├── ApiAiUser.ts │ │ │ ├── User.ts │ │ │ └── interfaces │ │ │ ├── IAction.ts │ │ │ ├── IApiAiUser.ts │ │ │ └── IUser.ts │ ├── localTunnel │ │ ├── interfaces │ │ │ └── ILocalTunnelService.ts │ │ ├── localTunnel.controller.ts │ │ ├── localTunnel.module.ts │ │ └── localTunnel.service.ts │ └── users │ │ ├── users.controller.ts │ │ └── users.module.ts └── server.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 10 | API_AI_CLIENT_ACCESS_TOKEN=azdfhsuidvlfjk:l,:;dzfqershkj234 -------------------------------------------------------------------------------- /.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 | migrate.js 10 | migrate.js.map 11 | 12 | # dependencies 13 | /node_modules 14 | 15 | # IDE 16 | /.idea 17 | /.awcache 18 | /.vscode 19 | 20 | # misc 21 | npm-debug.log 22 | 23 | # build 24 | /build 25 | 26 | # tests 27 | /coverage 28 | /.nyc_output 29 | 30 | #Package 31 | package-lock.json -------------------------------------------------------------------------------- /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 | ### [UNMAINTAINED] use [nestjs-dialogflow](https://github.com/adrien2p/nestjs-dialogflow) instead 2 | 3 | ![Nest](assets/logo.png) 4 | 5 | This project allow you to catch web hook from [api.ai](https://api.ai/) when a request is triggered 6 | on you agent, the server process the result to send back the response and let your assistant speech it. 7 | 8 | For the authentication part, when the user is authenticated with google OAuth2, The middleware catch the request 9 | to create the user locally if is not already created. 10 | 11 | ### How it works 12 | 13 | - To run lint and fix `npm run lint` 14 | - To run tests suite `npm run test` 15 | - Start the server `npm start` 16 | - To run up/down migration `npm run migrate {up/down}` 17 | 18 | ### Configuration 19 | 20 | To configure put all config file in the `./src/config/*`. 21 | To use the env variable, remove `.demo` from `.env.demo`. 22 | 23 | ### WebHook from [api.ai](https://api.ai/) 24 | 25 | To receive a callback from [api.ai](https://api.ai/), you must enabled webhook in `Fullfillment` tab and fill all information needed. 26 | To see how it works locally, you need to activate a local tunnel using the following url 27 | 28 | - `GET http://localhost:3000/localTunnelStart` to start it 29 | - `GET http://localhost:3000/localTunnelStop` to close it 30 | 31 | The local tunnel url returned need to be write in the `url` section with the path to the webHook which is actually `/apiAi` 32 | 33 | After to be logged in your app you should give you `token` access in the `headers` section : `authorization ...` 34 | Then, active `Fullfillment` in the targeted intent. 35 | 36 | ![fullfillment](assets/fulfillment.png) 37 | 38 | After that, to connect your agent to actions on google, go to `Integration` part and active actions on google. 39 | Then configure it to access `OAuth2` authentication with google. (Look `google cloud platform` to manage you OAuth settings). 40 | 41 | After [api.ai](https://api.ai/) received request, you should received a callback as the following result : 42 | ```json 43 | { 44 | "source": "agent", 45 | "resolvedQuery": "quelle est la météo", 46 | "action": "weather", 47 | "actionIncomplete": true, 48 | "parameters": { 49 | "address": "", 50 | "date-time": "" 51 | }, 52 | "contexts": [ 53 | { 54 | "name": "weather_dialog_context", 55 | "parameters": { 56 | "date-time.original": "", 57 | "address": "", 58 | "date-time": "", 59 | "address.original": "" 60 | }, 61 | "lifespan": 2 62 | }, 63 | { 64 | "name": "db44589c-29e2-447c-8368-94503e3ccae9_id_dialog_context", 65 | "parameters": { 66 | "date-time.original": "", 67 | "address": "", 68 | "date-time": "", 69 | "address.original": ""__ 70 | }, 71 | "lifespan": 2 72 | }, 73 | { 74 | "name": "weather_dialog_params_address", 75 | "parameters": { 76 | "date-time.original": "", 77 | "address": "", 78 | "date-time": "", 79 | "address.original": "" 80 | }, 81 | "lifespan": 1 82 | } 83 | ] 84 | } 85 | ``` 86 | 87 | And then, the result is sent to the run method of the service `jeeves` endPoint which manage the rest of the process. 88 | 89 | And when the process is done, a response is generated to be send to the `api.api`. 90 | -------------------------------------------------------------------------------- /assets/fulfillment.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adrien2p/nestjs-api-ai/0eeed8c823a3f7826860f5434dbb710e87d3ffbd/assets/fulfillment.png -------------------------------------------------------------------------------- /assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adrien2p/nestjs-api-ai/0eeed8c823a3f7826860f5434dbb710e87d3ffbd/assets/logo.png -------------------------------------------------------------------------------- /migrate.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('dotenv').config(); 4 | 5 | const path = require('path'); 6 | const childProcess = require('child_process'); 7 | const Promise = require('bluebird'); 8 | const Umzug = require('umzug'); 9 | 10 | import { sequelize } from './src/modules/common/index'; 11 | 12 | const DB_NAME = process.env.DB_NAME; 13 | const DB_USER = process.env.DB_USER; 14 | 15 | const umzug = new Umzug({ 16 | storage: 'sequelize', 17 | storageOptions: { 18 | sequelize: sequelize 19 | }, 20 | 21 | // see: https://github.com/sequelize/umzug/issues/17 22 | migrations: { 23 | params: [ 24 | sequelize.getQueryInterface(), // queryInterface 25 | sequelize.constructor, // DataTypes 26 | function () { 27 | throw new Error('Migration tried to use old style "done" callback. Please upgrade to "umzug" and return a promise instead.'); 28 | } 29 | ], 30 | path: './src/migrations', 31 | pattern: /\.ts$/ 32 | }, 33 | 34 | logging: function () { 35 | console.log.apply(null, arguments); 36 | } 37 | }); 38 | 39 | function logUmzugEvent (eventName) { 40 | return function (name, migration) { 41 | console.log(`${ name } ${ eventName }`); 42 | }; 43 | } 44 | umzug.on('migrating', logUmzugEvent('migrating')); 45 | umzug.on('migrated', logUmzugEvent('migrated')); 46 | umzug.on('reverting', logUmzugEvent('reverting')); 47 | umzug.on('reverted', logUmzugEvent('reverted')); 48 | 49 | function cmdStatus () { 50 | let result: any = {}; 51 | 52 | return umzug.executed() 53 | .then(executed => { 54 | result.executed = executed; 55 | return umzug.pending(); 56 | }).then(pending => { 57 | result.pending = pending; 58 | return result; 59 | }).then(({ executed, pending }) => { 60 | 61 | executed = executed.map(m => { 62 | m.name = path.basename(m.file, '.ts'); 63 | return m; 64 | }); 65 | pending = pending.map(m => { 66 | m.name = path.basename(m.file, '.ts'); 67 | return m; 68 | }); 69 | 70 | const current = executed.length > 0 ? executed[0].file : ''; 71 | const status = { 72 | current: current, 73 | executed: executed.map(m => m.file), 74 | pending: pending.map(m => m.file) 75 | }; 76 | 77 | console.log(JSON.stringify(status, null, 2)); 78 | 79 | return { executed, pending }; 80 | }); 81 | } 82 | 83 | function cmdMigrate () { 84 | return umzug.up(); 85 | } 86 | 87 | function cmdMigrateNext () { 88 | return cmdStatus() 89 | .then(({ executed, pending }) => { 90 | if (pending.length === 0) { 91 | return Promise.reject(new Error('No pending migrations')); 92 | } 93 | const next = pending[0].name; 94 | return umzug.up({ to: next }); 95 | }); 96 | } 97 | 98 | function cmdReset () { 99 | return umzug.down({ to: 0 }); 100 | } 101 | 102 | function cmdResetPrev () { 103 | return cmdStatus() 104 | .then(({ executed, pending }) => { 105 | if (executed.length === 0) { 106 | return Promise.reject(new Error('Already at initial state')); 107 | } 108 | const prev = executed[executed.length - 1].name; 109 | return umzug.down({ to: prev }); 110 | }); 111 | } 112 | 113 | function cmdHardReset () { 114 | return new Promise((resolve, reject) => { 115 | setImmediate(() => { 116 | try { 117 | console.log(`dropdb ${ DB_NAME }`); 118 | childProcess.spawnSync(`dropdb ${ DB_NAME }`); 119 | console.log(`createdb ${ DB_NAME } --username ${ DB_USER }`); 120 | childProcess.spawnSync(`createdb ${ DB_NAME } --username ${ DB_USER }`); 121 | resolve(); 122 | } catch (e) { 123 | console.log(e); 124 | reject(e); 125 | } 126 | }); 127 | }); 128 | } 129 | 130 | const cmd = process.argv[2].trim(); 131 | let executedCmd; 132 | 133 | console.log(`${ cmd.toUpperCase() } BEGIN`); 134 | switch (cmd) { 135 | case 'status': 136 | executedCmd = cmdStatus(); 137 | break; 138 | 139 | case 'up': 140 | case 'migrate': 141 | executedCmd = cmdMigrate(); 142 | break; 143 | 144 | case 'next': 145 | case 'migrate-next': 146 | executedCmd = cmdMigrateNext(); 147 | break; 148 | 149 | case 'down': 150 | case 'reset': 151 | executedCmd = cmdReset(); 152 | break; 153 | 154 | case 'prev': 155 | case 'reset-prev': 156 | executedCmd = cmdResetPrev(); 157 | break; 158 | 159 | case 'reset-hard': 160 | executedCmd = cmdHardReset(); 161 | break; 162 | 163 | default: 164 | console.log(`invalid cmd: ${ cmd }`); 165 | process.exit(1); 166 | } 167 | 168 | executedCmd 169 | .then((result) => { 170 | const doneStr = `${ cmd.toUpperCase() } DONE`; 171 | console.log(doneStr); 172 | console.log('=============================================================================='); 173 | }) 174 | .catch(err => { 175 | const errorStr = `${ cmd.toUpperCase() } ERROR`; 176 | console.log(errorStr); 177 | console.log('=============================================================================='); 178 | console.log(err); 179 | console.log('=============================================================================='); 180 | }) 181 | .then(() => { 182 | if (cmd !== 'status' && cmd !== 'reset-hard') { 183 | return cmdStatus(); 184 | } 185 | return Promise.resolve(); 186 | }) 187 | .then(() => process.exit(0)); 188 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nest-js-api-ai", 3 | "version": "3.0.0", 4 | "description": "Nest + Api.ai", 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 | "api-ai", 19 | "assistant", 20 | "dialogFlow" 21 | "ai" 22 | ], 23 | "author": { 24 | "name": "Adrien de Peretti", 25 | "email": "adrien.deperetti@gmail.com" 26 | }, 27 | "license": "MIT", 28 | "bugs": { 29 | "url": "https://github.com/adrien2p/nest-js-api-ai/issues" 30 | }, 31 | "scripts": { 32 | "lint": "tslint -c tslint.json -p tsconfig.json -e 'node_modules/**' -e '**/*.d.ts' -e 'src/**/*.test.ts' --type-check --fix 'src/**/*.ts'", 33 | "migrate": "ts-node migrate.ts", 34 | "start": "ts-node src/server.ts --env=NODE_ENV", 35 | "test": "mocha -r ts-node/register src/**/tests/*.ts" 36 | }, 37 | "dependencies": { 38 | "@nestjs/common": "^4.0.2", 39 | "@nestjs/core": "^4.0.3", 40 | "@nestjs/microservices": "^4.0.2", 41 | "@nestjs/testing": "^4.0.2", 42 | "@nestjs/websockets": "^4.0.2", 43 | "@types/express": "^4.0.37", 44 | "@types/geojson": "^1.0.4", 45 | "@types/jsonwebtoken": "^7.2.3", 46 | "@types/lodash": "^4.14.76", 47 | "@types/reflect-metadata": "0.0.5", 48 | "@types/sequelize": "^4.0.75", 49 | "@types/validator": "^6.3.0", 50 | "apollo-server-express": "^1.1.2", 51 | "bluebird": "^3.5.0", 52 | "body-parser": "^1.18.2", 53 | "codelyzer": "^3.2.0", 54 | "dotenv": "^4.0.0", 55 | "express": "^4.16.1", 56 | "jsonwebtoken": "^7.4.3", 57 | "localtunnel": "^1.8.3", 58 | "lodash": "^4.17.4", 59 | "pg": "^7.3.0", 60 | "pg-hstore": "^2.3.2", 61 | "redis": "^2.8.0", 62 | "reflect-metadata": "^0.1.10", 63 | "rxjs": "^5.4.3", 64 | "sequelize": "^4.13.0", 65 | "sequelize-typescript": "^0.5.0", 66 | "typescript": "^2.5.3", 67 | "umzug": "^2.0.1" 68 | }, 69 | "devDependencies": { 70 | "@types/chai": "^4.0.4", 71 | "@types/mocha": "^2.2.43", 72 | "@types/node": "^8.0.31", 73 | "chai": "^4.1.2", 74 | "mocha": "^3.5.3", 75 | "ts-node": "^3.3.0", 76 | "tslint": "^5.7.0", 77 | "tslint-config-standard": "^6.0.1" 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/modules/api.ai/actions/abstractAction.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { IAbstractAction } from '../interfaces/IAbstractAction'; 4 | import { sequelize , Action, ApiAiUser } from '../../common/index'; 5 | 6 | export abstract class AbstractAction implements IAbstractAction { 7 | public data: any; 8 | protected agentName: string; 9 | 10 | /** 11 | * @description: Allow to save some information and the request data. 12 | * @param {string} agentName 13 | * @param {IApiAiUserInstance} apiAiUser 14 | * @return {Promise} 15 | */ 16 | protected async save (agentName: string, response: any, apiAiUser: ApiAiUser): Promise { 17 | const obj: Action = { 18 | apiAiUserId: apiAiUser.id, 19 | agentName: agentName, 20 | actionName: this.data.result.action, 21 | requestId: this.data.id, 22 | data: this.data, 23 | response: response 24 | } as Action; 25 | 26 | await sequelize.transaction(t => Action.create(obj, { transaction: t })); 27 | } 28 | 29 | /** 30 | * @description: Allow to run the debug mode and show trace in the console. 31 | */ 32 | protected debug () { 33 | console.log('===================================================================='); 34 | console.log('= DEBUG MODE ='); 35 | console.log('===================================================================='); 36 | console.log('= REQUEST ='); 37 | console.log('===================================================================='); 38 | console.log(this.data); 39 | console.log('===================================================================='); 40 | console.log('= RESULT ='); 41 | console.log('===================================================================='); 42 | console.log(this.data.result); 43 | console.log('===================================================================='); 44 | console.log('= ORIGINAL REQUEST ='); 45 | console.log('===================================================================='); 46 | console.log(this.data.originalRequest); 47 | console.log('===================================================================='); 48 | console.log('= ORIGINAL REQUEST DATA ='); 49 | console.log('===================================================================='); 50 | console.log(this.data.originalRequest.data); 51 | console.log('===================================================================='); 52 | console.log('= ORIGINAL REQUEST DATA USER ='); 53 | console.log('===================================================================='); 54 | console.log(this.data.originalRequest.data.user); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/modules/api.ai/actions/jeeves.service.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { Component } from '@nestjs/common'; 4 | import { AbstractAction } from './abstractAction'; 5 | import { IResponse } from '../interfaces/IResponse'; 6 | import { IJeevesService } from '../interfaces/IJeevesService'; 7 | import { ApiAiUser } from '../../common/models/ApiAiUser'; 8 | 9 | @Component() 10 | export class JeevesService extends AbstractAction implements IJeevesService { 11 | private readonly debugActionName = 'theater.debug'; 12 | 13 | constructor () { 14 | super(); 15 | } 16 | 17 | public async run (action: string, apiAiUser: ApiAiUser): Promise { 18 | if (action === this.debugActionName) { 19 | this.debug(); 20 | const apiAiUserFullName = `${apiAiUser.firstName} ${apiAiUser.lastName}`; 21 | const response = { 22 | displayText: 'Debug mode is running, look at the console ' + apiAiUserFullName, 23 | speech: 'Debug mode is running, look at the console ' + apiAiUserFullName 24 | }; 25 | await this.save('jeeves', response, apiAiUser); 26 | return response; 27 | } 28 | 29 | /* Default response. */ 30 | return { 31 | displayText: 'You should ask me every thing', 32 | speech : 'You should ask me every thing' 33 | }; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/modules/api.ai/apiAi.controller.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { Controller, HttpStatus, Post, Request, Response } from '@nestjs/common'; 4 | import { JeevesService } from "./actions/jeeves.service"; 5 | 6 | @Controller() 7 | export class ApiAiController { 8 | constructor (private jeevesService: JeevesService) { } 9 | 10 | @Post('jeeves') 11 | public async jeeves (@Request() req, @Response() res) { 12 | if (!req.body) throw new Error('Missing body in the request : jeeves.'); 13 | 14 | const apiAiUser = req['apiAiUser']; 15 | this.jeevesService.data = req.body; 16 | const response = this.jeevesService.run(req.body.result.action.toLowerCase(), apiAiUser); 17 | return res.status(HttpStatus.OK).json(response); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/modules/api.ai/apiAi.module.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { Module, RequestMethod } from '@nestjs/common'; 4 | import { MiddlewaresConsumer } from '@nestjs/common/interfaces/middlewares'; 5 | import { 6 | ApiAiJeevesMiddleware, 7 | ApiAiJeevesCheckOrCreateUserMiddleware 8 | } from '../common/index'; 9 | import { ApiAiController } from './apiAi.controller'; 10 | import { JeevesService } from './actions/jeeves.service'; 11 | 12 | @Module({ 13 | controllers: [ApiAiController], 14 | components: [ 15 | JeevesService 16 | ], 17 | modules: [], 18 | exports: [] 19 | }) 20 | export class ApiAiModule { 21 | configure (consumer: MiddlewaresConsumer) { 22 | consumer.apply([ 23 | ApiAiJeevesMiddleware, 24 | ApiAiJeevesCheckOrCreateUserMiddleware 25 | ]).forRoutes({ 26 | path: 'jeeves', method: RequestMethod.ALL 27 | }); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/modules/api.ai/interfaces/IAbstractAction.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | export interface IAbstractAction { 4 | data: any; 5 | } 6 | -------------------------------------------------------------------------------- /src/modules/api.ai/interfaces/IActionService.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { IActionServiceResponse } from './IActionServiceResponse'; 4 | 5 | export interface IActionService { 6 | /** 7 | * @description: Find the action related to the request made by the user. 8 | * @param {any} data 9 | * @return {IActionServiceResponse} 10 | */ 11 | run (data: any): Promise; 12 | } 13 | -------------------------------------------------------------------------------- /src/modules/api.ai/interfaces/IActionServiceResponse.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | export interface IActionServiceResponse { 4 | speech: string; 5 | displayText: string; 6 | source: string; 7 | } 8 | -------------------------------------------------------------------------------- /src/modules/api.ai/interfaces/IInterceptorService.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { IResponse } from './IResponse'; 4 | import { ApiAiUser } from '../../common/models/ApiAiUser'; 5 | 6 | export interface IInterceptorService { 7 | /** 8 | * @description: Parse data to redirect to the right action and return the response from the action. 9 | * @param data 10 | * @param {IApiAiUser} apiAiUser 11 | * @return {IResponse} 12 | */ 13 | parse (data: any, apiAiUser: ApiAiUser): IResponse; 14 | } 15 | -------------------------------------------------------------------------------- /src/modules/api.ai/interfaces/IJeevesService.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { IResponse } from './IResponse'; 4 | import { IAbstractAction } from './IAbstractAction'; 5 | import { ApiAiUser } from '../../common/models/ApiAiUser'; 6 | 7 | export interface IJeevesService extends IAbstractAction { 8 | /** 9 | * @description: Get an action name and process to return the right response. 10 | * @param {string} action 11 | * @return {IResponse} 12 | */ 13 | run (action: string, apiAiUser: ApiAiUser): Promise; 14 | } 15 | -------------------------------------------------------------------------------- /src/modules/api.ai/interfaces/IResponse.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | export interface IResponse { 4 | displayText: string; 5 | speech: string; 6 | } 7 | -------------------------------------------------------------------------------- /src/modules/app.module.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { Module } from '@nestjs/common'; 4 | import { UsersModule } from './users/users.module'; 5 | import { AuthModule } from './auth/auth.module'; 6 | import { LocalTunnelModule } from './localTunnel/localTunnel.module'; 7 | import { ApiAiModule } from './api.ai/apiAi.module'; 8 | 9 | @Module({ 10 | controllers: [], 11 | components: [], 12 | modules: [ 13 | UsersModule, 14 | AuthModule, 15 | LocalTunnelModule, 16 | ApiAiModule 17 | ], 18 | exports: [] 19 | }) 20 | export class ApplicationModule { } 21 | -------------------------------------------------------------------------------- /src/modules/auth/auth.controller.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { Controller, Post, HttpStatus, Request, Response } from '@nestjs/common'; 4 | import { MessageCodeError } from '../common/index'; 5 | import { AuthService } from './auth.service'; 6 | 7 | @Controller() 8 | export class AuthController { 9 | constructor (private authService: AuthService) { } 10 | 11 | @Post('login') 12 | public async login (@Request() req, @Response() res) { 13 | const body = req.body; 14 | if (!body) throw new MessageCodeError('auth:login:missingInformation'); 15 | if (!body.email) throw new MessageCodeError('auth:login:missingEmail'); 16 | if (!body.password) throw new MessageCodeError('auth:login:missingPassword'); 17 | 18 | const token = await this.authService.sign(body); 19 | res.status(HttpStatus.ACCEPTED).json('Bearer ' + token); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/modules/auth/auth.module.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { Module } from '@nestjs/common'; 4 | import { AuthService } from './auth.service'; 5 | import { AuthController } from './auth.controller'; 6 | 7 | @Module({ 8 | controllers: [AuthController], 9 | components: [AuthService], 10 | modules: [], 11 | exports: [] 12 | }) 13 | export class AuthModule { } 14 | -------------------------------------------------------------------------------- /src/modules/auth/auth.service.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { Component } from '@nestjs/common'; 4 | import * as crypto from 'crypto'; 5 | import * as jwt from 'jsonwebtoken'; 6 | import { MessageCodeError, User } from '../common/index'; 7 | import { IAuthService, IJwtOptions } from './interfaces/IAuthService'; 8 | 9 | @Component() 10 | export class AuthService implements IAuthService { 11 | private _options: IJwtOptions = { 12 | algorithm: 'HS256', 13 | expiresIn: '2 days', 14 | jwtid: process.env.JWT_ID || '' 15 | }; 16 | 17 | get options (): IJwtOptions { 18 | return this._options; 19 | } 20 | 21 | set options (value: IJwtOptions) { 22 | this._options.algorithm = value.algorithm; 23 | } 24 | 25 | public async sign (credentials: { email: string, password: string }): Promise { 26 | const user = await User.findOne({ 27 | where: { 28 | email: credentials.email, 29 | password: crypto.createHmac('sha256', credentials.password).digest('hex') 30 | } 31 | }); 32 | if (!user) throw new MessageCodeError('user:notFound'); 33 | 34 | const payload = { 35 | id: user.id, 36 | email: user.email 37 | }; 38 | 39 | return await jwt.sign(payload, process.env.JWT_KEY || '', this._options); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/modules/auth/interfaces/IAuthService.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | export interface IAuthService { 4 | options: IJwtOptions; 5 | 6 | /** 7 | * @description: Sign the user, create a new token before it insert in the response header Authorization. 8 | * @param {email: string; password: string} credentials 9 | * @return {Promise} 10 | */ 11 | sign (credentials: { email: string, password: string }): Promise; 12 | } 13 | 14 | export interface IJwtOptions { 15 | algorithm: string; 16 | expiresIn: number | string; 17 | jwtid: string; 18 | } 19 | -------------------------------------------------------------------------------- /src/modules/auth/tests/auth.service.test.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('dotenv').config(); 4 | 5 | import 'mocha'; 6 | import { expect } from 'chai'; 7 | import { Sequelize } from 'sequelize-typescript'; 8 | import { fakeUser } from './fixtures/fake.data'; 9 | import { databaseConfig, User } from '../../common/index'; 10 | import { AuthService } from '../auth.service'; 11 | 12 | describe('AuthService should', () => { 13 | let authService; 14 | let user; 15 | let sequelize; 16 | 17 | before(async () => { 18 | authService = new AuthService(); 19 | sequelize = new Sequelize(databaseConfig.test); 20 | 21 | /* Create a new user for the test. */ 22 | await sequelize.transaction(async t => { 23 | return user = await User.create(fakeUser, { 24 | transaction: t, 25 | returning: true 26 | }); 27 | }); 28 | }); 29 | 30 | after(async () => { 31 | /* Remove the previous created user. */ 32 | await sequelize.transaction(async t => { 33 | return await User.destroy({ 34 | where: { id: user.id }, 35 | force: true, 36 | transaction: t 37 | }); 38 | }); 39 | }); 40 | 41 | it('allow to sign a new token and throw without user found', async () => { 42 | let error; 43 | try { 44 | await authService.sign({ 45 | email: 'undefined@undefined.fr', 46 | password: 'password' 47 | }); 48 | } catch (err) { 49 | error = err; 50 | } 51 | 52 | expect(error).not.null; 53 | expect(error.httpStatus).equal(404); 54 | expect(error.messageCode).equal('user:notFound'); 55 | expect(error.errorMessage).equal('Unable to found the user with the provided information.'); 56 | expect(error.message).equal('Aucun utilisateur trouvé avec les informations fourni.'); 57 | }); 58 | 59 | it('allow to sign a new token and return it', async () => { 60 | const token = await authService.sign({ 61 | email: fakeUser.email, 62 | password: fakeUser.password 63 | }); 64 | 65 | expect(token).not.null; 66 | expect(token).to.be.string; 67 | }); 68 | }); 69 | -------------------------------------------------------------------------------- /src/modules/auth/tests/fixtures/fake.data.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | export const fakeUser: any = { 4 | firstName: 'test', 5 | lastName: 'test', 6 | email: 'test@test.fr', 7 | password: 'test' 8 | }; 9 | -------------------------------------------------------------------------------- /src/modules/common/config/database.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import * as path from 'path'; 4 | import { IDatabaseConfig } from './interfaces/IDatabase'; 5 | import { Sequelize } from 'sequelize-typescript'; 6 | 7 | export const databaseConfig: IDatabaseConfig = { 8 | development: { 9 | username: process.env.DB_USER || '', 10 | password: process.env.DB_PASSWORD || '', 11 | database: process.env.DB_NAME || '', 12 | host: process.env.DB_HOST || '127.0.0.1', 13 | port: Number(process.env.DB_PORT) || 5432, 14 | dialect: 'postgres', 15 | logging: false, 16 | force: true, 17 | timezone: '+02:00', 18 | modelPaths: [ 19 | path.resolve(__dirname, '../models') 20 | ] 21 | }, 22 | production: { 23 | username: process.env.DB_USER || '', 24 | password: process.env.DB_PASSWORD || '', 25 | database: process.env.DB_NAME || '', 26 | host: process.env.DB_HOST || '127.0.0.1', 27 | port: Number(process.env.DB_PORT) || 5432, 28 | dialect: 'postgres', 29 | logging: false, 30 | force: true, 31 | timezone: '+02:00', 32 | modelPaths: [ 33 | path.resolve(__dirname, '../models') 34 | ] 35 | }, 36 | test: { 37 | username: process.env.DB_USER || '', 38 | password: process.env.DB_PASSWORD || '', 39 | database: process.env.DB_NAME || '', 40 | host: process.env.DB_HOST || '127.0.0.1', 41 | port: Number(process.env.DB_PORT) || 5432, 42 | dialect: 'postgres', 43 | logging: true, 44 | force: true, 45 | timezone: '+02:00', 46 | modelPaths: [ 47 | path.resolve(__dirname, '../models') 48 | ] 49 | } 50 | }; 51 | 52 | const config = !process.env.NODE_ENV || process.env.NODE_ENV === 'development' ? 53 | databaseConfig.development : 54 | databaseConfig.production; 55 | export const sequelize: Sequelize = new Sequelize(config); 56 | -------------------------------------------------------------------------------- /src/modules/common/config/errorMessages.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { HttpStatus } from '@nestjs/common'; 4 | import { IErrorMessages } from './interfaces/IErrorMessages'; 5 | 6 | export const errorMessagesConfig: { [messageCode: string]: IErrorMessages } = { 7 | 'user:missingInformation': { 8 | type: 'BadRequest', 9 | httpStatus: HttpStatus.BAD_REQUEST, 10 | errorMessage: 'Missing parameters on the user.', 11 | userMessage: "Des informations sont manquantes sur l'utilisateur." 12 | }, 13 | 'user:missingFirstName': { 14 | type: 'BadRequest', 15 | httpStatus: HttpStatus.BAD_REQUEST, 16 | errorMessage: 'Missing parameter first name on the user.', 17 | userMessage: 'Veuillez indiquer votre prénom.' 18 | }, 19 | 'user:missingLastName': { 20 | type: 'BadRequest', 21 | httpStatus: HttpStatus.BAD_REQUEST, 22 | errorMessage: 'Missing parameter last name on the user.', 23 | userMessage: 'Veuillez indiquer votre nom.' 24 | }, 25 | 'user:missingEmail': { 26 | type: 'BadRequest', 27 | httpStatus: HttpStatus.BAD_REQUEST, 28 | errorMessage: 'Missing parameter email on the user.', 29 | userMessage: 'Veuillez indiquer votre adresse e-mail.' 30 | }, 31 | 'user:missingPassword': { 32 | type: 'BadRequest', 33 | httpStatus: HttpStatus.BAD_REQUEST, 34 | errorMessage: 'Missing parameter password on the user.', 35 | userMessage: 'Veuillez indiquer votre mot de passe.' 36 | }, 37 | 'user:emailAlreadyExist': { 38 | type: 'BadRequest', 39 | httpStatus: HttpStatus.BAD_REQUEST, 40 | errorMessage: 'Warning, Email already exist.', 41 | userMessage: "L'adresse e-mail que vous avez fourni est déjà utilisé." 42 | }, 43 | 'user:missingId': { 44 | type: 'BadRequest', 45 | httpStatus: HttpStatus.BAD_REQUEST, 46 | errorMessage: 'Missing parameter id.', 47 | userMessage: 'Veuillez fournir un id valid.' 48 | }, 49 | 'user:notFound': { 50 | type: 'notFound', 51 | httpStatus: HttpStatus.NOT_FOUND, 52 | errorMessage: 'Unable to found the user with the provided information.', 53 | userMessage: 'Aucun utilisateur trouvé avec les informations fourni.' 54 | }, 55 | 'request:unauthorized': { 56 | type: 'unauthorized', 57 | httpStatus: HttpStatus.UNAUTHORIZED, 58 | errorMessage: 'Access unauthorized.', 59 | userMessage: 'Accès non autorisé.' 60 | }, 61 | 'auth:login:missingEmail': { 62 | type: 'BadRequest', 63 | httpStatus: HttpStatus.BAD_REQUEST, 64 | errorMessage: 'Unable to connect the user without email.', 65 | userMessage: 'Veuillez indiquer votre adresse e-mail.' 66 | }, 67 | 'auth:login:missingPassword': { 68 | type: 'BadRequest', 69 | httpStatus: HttpStatus.BAD_REQUEST, 70 | errorMessage: 'Unable to connect the user without password.', 71 | userMessage: 'Veuillez indiquer votre mot de passe.' 72 | }, 73 | 'action:missingAgentName': { 74 | type: 'BadRequest', 75 | httpStatus: HttpStatus.BAD_REQUEST, 76 | errorMessage: 'Missing parameter agent name on the action.', 77 | userMessage: "Veuillez indiquer le nom de l'agent concerné." 78 | }, 79 | 'action:missingActionName': { 80 | type: 'BadRequest', 81 | httpStatus: HttpStatus.BAD_REQUEST, 82 | errorMessage: 'Missing parameter action name on the action.', 83 | userMessage: "Veuillez indiquer le nom de l'action concerné." 84 | }, 85 | 'action:missingRequestId': { 86 | type: 'BadRequest', 87 | httpStatus: HttpStatus.BAD_REQUEST, 88 | errorMessage: 'Missing parameter request id on the action.', 89 | userMessage: "Veuillez indiquer l'identifiant de la requête concerné." 90 | }, 91 | 'action:missingApiAiUserId': { 92 | type: 'BadRequest', 93 | httpStatus: HttpStatus.BAD_REQUEST, 94 | errorMessage: 'Missing parameter request apiAiUserId on the action.', 95 | userMessage: "Veuillez indiquer l'identifiant de du api ai user." 96 | }, 97 | 'action:missingData': { 98 | type: 'BadRequest', 99 | httpStatus: HttpStatus.BAD_REQUEST, 100 | errorMessage: 'Missing parameter data on the action.', 101 | userMessage: "Veuillez indiquer les données de l'action concerné." 102 | } 103 | }; 104 | -------------------------------------------------------------------------------- /src/modules/common/config/global.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | export const config: any = { 4 | localTunnelPort: 3000 5 | }; 6 | -------------------------------------------------------------------------------- /src/modules/common/config/index.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | export * from './database'; 4 | export * from './errorMessages'; 5 | export * from './global'; 6 | -------------------------------------------------------------------------------- /src/modules/common/config/interfaces/IDatabase.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | export interface IDatabaseConfigAttributes { 4 | username: string; 5 | password: string; 6 | database: string; 7 | host: string; 8 | port: number; 9 | dialect: string; 10 | logging: boolean | Function; 11 | force: boolean; 12 | timezone: string; 13 | modelPaths: Array; 14 | } 15 | 16 | export interface IDatabaseConfig { 17 | development: IDatabaseConfigAttributes; 18 | production: IDatabaseConfigAttributes; 19 | test: IDatabaseConfigAttributes; 20 | } 21 | -------------------------------------------------------------------------------- /src/modules/common/config/interfaces/IErrorMessages.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { HttpStatus } from '@nestjs/common'; 4 | 5 | export interface IErrorMessages { 6 | type: string; 7 | httpStatus: HttpStatus; 8 | errorMessage: string; 9 | userMessage: string; 10 | } 11 | -------------------------------------------------------------------------------- /src/modules/common/filters/DispatchError.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { HttpException } from '@nestjs/core'; 4 | import { Catch, ExceptionFilter, HttpStatus } from '@nestjs/common'; 5 | import { MessageCodeError } from '../lib/error/MessageCodeError'; 6 | import { ValidationError } from 'sequelize'; 7 | 8 | @Catch(MessageCodeError, ValidationError, HttpException, Error) 9 | export class DispatchError implements ExceptionFilter { 10 | public catch (err, res) { 11 | console.log(err); 12 | 13 | if (err instanceof MessageCodeError) { 14 | /* MessageCodeError, Set all header variable to have a context for the client in case of MessageCodeError. */ 15 | res.setHeader('x-message-code-error', err.messageCode); 16 | res.setHeader('x-message', err.message); 17 | res.setHeader('x-httpStatus-error', err.httpStatus); 18 | 19 | return res.status(err.httpStatus).send(); 20 | } else if (err instanceof ValidationError) { 21 | /* Sequelize validation error. */ 22 | res.setHeader('x-message-code-error', (err as ValidationError).errors[0].type); 23 | res.setHeader('x-message', (err as ValidationError).errors[0].message); 24 | res.setHeader('x-httpStatus-error', HttpStatus.BAD_REQUEST); 25 | 26 | return res.status(HttpStatus.BAD_REQUEST).send(); 27 | } else { 28 | return res.status(HttpStatus.INTERNAL_SERVER_ERROR).send(); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/modules/common/index.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | export * from './config/index'; 4 | export * from './filters/DispatchError'; 5 | export * from './lib/index'; 6 | export * from './middlewares/index'; 7 | export * from './models/User'; 8 | export * from './models/Action'; 9 | export * from './models/ApiAiUser'; 10 | -------------------------------------------------------------------------------- /src/modules/common/lib/error/MessageCodeError.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { errorMessagesConfig } from '../../config/errorMessages'; 4 | import { IErrorMessages } from '../../config/interfaces/IErrorMessages'; 5 | 6 | export class MessageCodeError extends Error { 7 | public messageCode: string; 8 | public httpStatus: number; 9 | public errorMessage: string; 10 | 11 | constructor (messageCode: string) { 12 | super(); 13 | 14 | const errorMessageConfig = this.getMessageFromMessageCode(messageCode); 15 | if (!errorMessageConfig) throw new Error('Unable to find message code error.'); 16 | 17 | Error.captureStackTrace(this, this.constructor); 18 | this.name = this.constructor.name; 19 | this.httpStatus = errorMessageConfig.httpStatus; 20 | this.messageCode = messageCode; 21 | this.errorMessage = errorMessageConfig.errorMessage; 22 | this.message = errorMessageConfig.userMessage; 23 | } 24 | 25 | /** 26 | * @description: Find the error config by the given message code. 27 | * @param {string} messageCode 28 | * @return {IErrorMessages} 29 | */ 30 | private getMessageFromMessageCode (messageCode: string): IErrorMessages { 31 | let errorMessageConfig: IErrorMessages | undefined; 32 | Object.keys(errorMessagesConfig).some(key => { 33 | if (key === messageCode) { 34 | errorMessageConfig = errorMessagesConfig[key]; 35 | return true; 36 | } 37 | return false; 38 | }); 39 | 40 | if (!errorMessageConfig) throw new Error('Unable to find the given message code error.'); 41 | return errorMessageConfig; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/modules/common/lib/index.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | export * from './error/MessageCodeError'; 4 | -------------------------------------------------------------------------------- /src/modules/common/middlewares/apiAi.jeeves.checkOrCreateUser.middleware.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { Middleware, NestMiddleware } from '@nestjs/common'; 4 | import { Request, Response, NextFunction } from 'express'; 5 | import * as https from 'https'; 6 | import { sequelize, ApiAiUser } from '../index'; 7 | 8 | @Middleware() 9 | export class ApiAiJeevesCheckOrCreateUserMiddleware implements NestMiddleware { 10 | resolve () { 11 | return async function (req: Request, res: Response, next: NextFunction) { 12 | if (!req.body || (req.body && !req.body.originalRequest)) next(); 13 | 14 | const data = req.body; 15 | const accessToken = data.originalRequest.data.user.accessToken; 16 | const googleUserId = data.originalRequest.data.user.userId; 17 | const googleApiUrl = 'https://www.googleapis.com/oauth2/v1/userinfo?alt=json&access_token='; 18 | 19 | let apiAiUser = await ApiAiUser.findOne({ where: { googleUserId: googleUserId }}); 20 | await sequelize.transaction(async t => { 21 | if (!apiAiUser) { 22 | /* Get user info from google to create a new apiAiUser locally. */ 23 | const userInfo: any = await new Promise((resolve, reject) => { 24 | https.get(googleApiUrl + accessToken, res => { 25 | let streamData = ''; 26 | res.on('data', (d) => streamData += d); 27 | res.on('error', (err) => reject(err)); 28 | res.on('end', () => resolve(JSON.parse(streamData))); 29 | }); 30 | }); 31 | 32 | /* Create the new user. */ 33 | apiAiUser = await ApiAiUser.create({ 34 | firstName: userInfo.given_name, 35 | lastName: userInfo.family_name, 36 | email: userInfo.email, 37 | accessToken: accessToken, 38 | googleUserId: googleUserId 39 | }, { 40 | returning: true, 41 | transaction: t 42 | }); 43 | } else { 44 | await apiAiUser.update({ 45 | accessToken: accessToken 46 | }, { transaction: t }); 47 | await apiAiUser.reload(); 48 | } 49 | }); 50 | 51 | /* Put the user in the request to get it easily in the next action. */ 52 | req['apiAiUser'] = apiAiUser; 53 | next(); 54 | }; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/modules/common/middlewares/apiAi.jeeves.middleware.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { Middleware, NestMiddleware } from '@nestjs/common'; 4 | import { Request, Response, NextFunction } from 'express'; 5 | import { MessageCodeError } from '../lib/error/MessageCodeError'; 6 | 7 | @Middleware() 8 | export class ApiAiJeevesMiddleware implements NestMiddleware { 9 | resolve () { 10 | return async function (req: Request, res: Response, next: NextFunction) { 11 | if (!req.headers.authorization) throw new MessageCodeError('request:unauthorized'); 12 | 13 | if (process.env.API_AI_CLIENT_ACCESS_TOKEN_JEEVES === req.headers.authorization) { 14 | next(); 15 | } else { 16 | throw new MessageCodeError('request:unauthorized'); 17 | } 18 | }; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/modules/common/middlewares/auth.middleware.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { Middleware, NestMiddleware } from '@nestjs/common'; 4 | import { Request, Response, NextFunction } from 'express'; 5 | import * as jwt from 'jsonwebtoken'; 6 | import { MessageCodeError } from '../lib/error/MessageCodeError'; 7 | import { User } from '../index'; 8 | 9 | @Middleware() 10 | export class AuthMiddleware implements NestMiddleware { 11 | resolve () { 12 | return async function (req: Request, res: Response, next: NextFunction) { 13 | if (req.headers.authorization && (req.headers.authorization as string).split(' ')[0] === 'Bearer') { 14 | let token = (req.headers.authorization as string).split(' ')[1]; 15 | const decoded: any = jwt.verify(token, process.env.JWT_KEY || ''); 16 | const user = await User.findOne({ 17 | where: { 18 | id: decoded.id, 19 | email: decoded.email 20 | } 21 | }); 22 | if (!user) throw new MessageCodeError('request:unauthorized'); 23 | next(); 24 | } else { 25 | throw new MessageCodeError('request:unauthorized'); 26 | } 27 | }; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/modules/common/middlewares/index.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | export * from './auth.middleware'; 4 | export * from './apiAi.jeeves.middleware'; 5 | export * from './apiAi.jeeves.checkOrCreateUser.middleware'; 6 | -------------------------------------------------------------------------------- /src/modules/common/migrations/20170807000001-create-table-actions.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { sequelize } from '../index'; 4 | 5 | export async function up () { 6 | // language=PostgreSQL 7 | sequelize.query(` 8 | CREATE TABLE "actions" ( 9 | "id" SERIAL UNIQUE PRIMARY KEY NOT NULL, 10 | "apiAiUserId" INTEGER NOT NULL, 11 | "agentName" VARCHAR(30) NOT NULL, 12 | "actionName" VARCHAR(30) NOT NULL, 13 | "requestId" VARCHAR(100) UNIQUE NOT NULL, 14 | "data" JSONB NOT NULL, 15 | "response" JSONB NOT NULL, 16 | "createdAt" TIMESTAMP NOT NULL, 17 | "updatedAt" TIMESTAMP NOT NULL, 18 | "deletedAt" TIMESTAMP, 19 | 20 | FOREIGN KEY ("apiAiUserId") REFERENCES "apiAiUsers" (id) 21 | ); 22 | `); 23 | 24 | console.log('*Table actions created!*'); 25 | } 26 | 27 | export async function down () { 28 | // language=PostgreSQL 29 | sequelize.query(`DROP TABLE actions`); 30 | } 31 | -------------------------------------------------------------------------------- /src/modules/common/migrations/20170807000001-create-table-users.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { sequelize } from '../index'; 4 | 5 | export async function up () { 6 | // language=PostgreSQL 7 | sequelize.query(` 8 | CREATE TABLE "users" ( 9 | "id" SERIAL UNIQUE PRIMARY KEY NOT NULL, 10 | "firstName" VARCHAR(30) NOT NULL, 11 | "lastName" VARCHAR(30) NOT NULL, 12 | "email" VARCHAR(100) UNIQUE NOT NULL, 13 | "password" TEXT NOT NULL, 14 | "birthday" TIMESTAMP, 15 | "createdAt" TIMESTAMP NOT NULL, 16 | "updatedAt" TIMESTAMP NOT NULL, 17 | "deletedAt" TIMESTAMP 18 | ); 19 | `); 20 | 21 | console.log('*Table users created!*'); 22 | } 23 | 24 | export async function down () { 25 | // language=PostgreSQL 26 | sequelize.query(`DROP TABLE users`); 27 | } 28 | -------------------------------------------------------------------------------- /src/modules/common/migrations/20170828000001-create-table-apiAiUsers.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { sequelize } from '../index'; 4 | 5 | export async function up () { 6 | // language=PostgreSQL 7 | sequelize.query(` 8 | CREATE TABLE "apiAiUsers" ( 9 | "id" SERIAL UNIQUE PRIMARY KEY NOT NULL, 10 | "firstName" VARCHAR(30) NOT NULL, 11 | "lastName" VARCHAR(30) NOT NULL, 12 | "email" VARCHAR(100) UNIQUE NOT NULL, 13 | "accessToken" TEXT UNIQUE NOT NULL, 14 | "googleUserId" TEXT UNIQUE NOT NULL, 15 | "createdAt" TIMESTAMP NOT NULL, 16 | "updatedAt" TIMESTAMP NOT NULL, 17 | "deletedAt" TIMESTAMP 18 | ); 19 | `); 20 | 21 | console.log('*Table apiAiUsers created!*'); 22 | } 23 | 24 | export async function down () { 25 | // language=PostgreSQL 26 | sequelize.query(`DROP TABLE "apiAiUsers"`); 27 | } 28 | -------------------------------------------------------------------------------- /src/modules/common/models/Action.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { 4 | Table, Column, Model, DataType, 5 | CreatedAt, UpdatedAt, DeletedAt, BeforeValidate, ForeignKey 6 | } from 'sequelize-typescript'; 7 | import { IDefineOptions } from 'sequelize-typescript/lib/interfaces/IDefineOptions'; 8 | import { MessageCodeError } from '../lib/error/MessageCodeError'; 9 | import { ApiAiUser } from './ApiAiUser'; 10 | 11 | const tableOptions: IDefineOptions = { timestamp: true, tableName: 'actions' } as IDefineOptions; 12 | 13 | @Table(tableOptions) 14 | export class Action extends Model { 15 | @Column({ 16 | type: DataType.NUMERIC, 17 | allowNull: false, 18 | autoIncrement: true, 19 | unique: true, 20 | primaryKey: true 21 | }) 22 | id: number; 23 | 24 | @Column({ 25 | type: DataType.NUMERIC, 26 | allowNull: false 27 | }) 28 | @ForeignKey(() => ApiAiUser) 29 | apiAiUserId: number; 30 | 31 | @Column({ 32 | type: DataType.TEXT, 33 | allowNull: false 34 | }) 35 | agentName: string; 36 | 37 | @Column({ 38 | type: DataType.TEXT, 39 | allowNull: false 40 | }) 41 | actionName: string; 42 | 43 | @Column({ 44 | type: DataType.INTEGER, 45 | allowNull: false 46 | }) 47 | requestId: number; 48 | 49 | @Column({ 50 | type: DataType.JSONB, 51 | allowNull: false 52 | }) 53 | data: any; 54 | 55 | @Column({ 56 | type: DataType.JSONB, 57 | allowNull: false 58 | }) 59 | response: any; 60 | 61 | @CreatedAt 62 | createdAt: Date; 63 | 64 | @UpdatedAt 65 | updatedAt: Date; 66 | 67 | @DeletedAt 68 | deletedAt: Date; 69 | 70 | @BeforeValidate 71 | static validateData (action: Action, options: any) { 72 | if (!options.transaction) throw new Error('Missing transaction.'); 73 | if (!action.agentName) throw new MessageCodeError('action:missingAgentName'); 74 | if (!action.actionName) throw new MessageCodeError('action:missingActionName'); 75 | if (!action.requestId) throw new MessageCodeError('action:missingRequestId'); 76 | if (!action.apiAiUserId) throw new MessageCodeError('action:missingApiAiUserId'); 77 | if (!action.data) throw new MessageCodeError('action:missingData'); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/modules/common/models/ApiAiUser.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { 4 | Table, Column, Model, DataType, 5 | CreatedAt, UpdatedAt, DeletedAt, HasMany 6 | } from 'sequelize-typescript'; 7 | import { IDefineOptions } from 'sequelize-typescript/lib/interfaces/IDefineOptions'; 8 | import { Action } from './Action'; 9 | 10 | const tableOptions: IDefineOptions = { timestamp: true, tableName: 'apiAiUsers' } as IDefineOptions; 11 | 12 | @Table(tableOptions) 13 | export class ApiAiUser extends Model { 14 | @Column({ 15 | type: DataType.NUMERIC, 16 | allowNull: false, 17 | autoIncrement: true, 18 | unique: true, 19 | primaryKey: true 20 | }) 21 | id: number; 22 | 23 | @Column({ 24 | type: DataType.CHAR(30), 25 | allowNull: false 26 | }) 27 | firstName: string; 28 | 29 | @Column({ 30 | type: DataType.CHAR(30), 31 | allowNull: false 32 | }) 33 | lastName: string; 34 | 35 | @Column({ 36 | type: DataType.CHAR(100), 37 | allowNull: false, 38 | validate: { 39 | isEmail: true, 40 | unique: "L'adresse e-mail fourni existe déjà, veuillez en choisir une autre." 41 | } 42 | }) 43 | email: string; 44 | 45 | @Column({ 46 | type: DataType.TEXT, 47 | allowNull: false 48 | }) 49 | accessToken: string; 50 | 51 | @Column({ 52 | type: DataType.TEXT, 53 | allowNull: false 54 | }) 55 | googleUserId: string; 56 | 57 | @CreatedAt 58 | createdAt: Date; 59 | 60 | @UpdatedAt 61 | updatedAt: Date; 62 | 63 | @DeletedAt 64 | deletedAt: Date; 65 | 66 | @HasMany(() => Action, { 67 | as: 'actions', 68 | foreignKey: 'apiAiUserId' 69 | }) 70 | actions: Array; 71 | } 72 | -------------------------------------------------------------------------------- /src/modules/common/models/User.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import * as crypto from 'crypto'; 4 | import { 5 | Table, Column, Model, DataType, 6 | CreatedAt, UpdatedAt, DeletedAt, BeforeValidate, BeforeCreate 7 | } from 'sequelize-typescript'; 8 | import { IDefineOptions } from 'sequelize-typescript/lib/interfaces/IDefineOptions'; 9 | import { MessageCodeError } from '../lib/error/MessageCodeError'; 10 | 11 | const tableOptions: IDefineOptions = { timestamp: true, tableName: 'users' } as IDefineOptions; 12 | 13 | @Table(tableOptions) 14 | export class User extends Model { 15 | @Column({ 16 | type: DataType.NUMERIC, 17 | allowNull: false, 18 | autoIncrement: true, 19 | unique: true, 20 | primaryKey: true 21 | }) 22 | id: number; 23 | 24 | @Column({ 25 | type: DataType.CHAR(30), 26 | allowNull: false 27 | }) 28 | firstName: string; 29 | 30 | @Column({ 31 | type: DataType.CHAR(30), 32 | allowNull: false 33 | }) 34 | lastName: string; 35 | 36 | @Column({ 37 | type: DataType.CHAR(100), 38 | allowNull: false, 39 | validate: { 40 | isEmail: true, 41 | isUnique: async (value: string, next: Function): Promise => { 42 | const isExist = await User.findOne({ where: { email: value }}); 43 | if (isExist) { 44 | const error = new MessageCodeError('user:create:emailAlreadyExist'); 45 | next(error); 46 | } 47 | next(); 48 | } 49 | } 50 | }) 51 | email: string; 52 | 53 | @Column({ 54 | type: DataType.TEXT, 55 | allowNull: false 56 | }) 57 | password: string; 58 | 59 | @Column({ type: DataType.DATE }) 60 | birthday: Date; 61 | 62 | @CreatedAt 63 | createdAt: Date; 64 | 65 | @UpdatedAt 66 | updatedAt: Date; 67 | 68 | @DeletedAt 69 | deletedAt: Date; 70 | 71 | @BeforeValidate 72 | static validateData (user: User, options: any) { 73 | if (!options.transaction) throw new Error('Missing transaction.'); 74 | if (!user.firstName) throw new MessageCodeError('user:create:missingFirstName'); 75 | if (!user.lastName) throw new MessageCodeError('user:create:missingLastName'); 76 | if (!user.email) throw new MessageCodeError('user:create:missingEmail'); 77 | if (!user.password) throw new MessageCodeError('user:create:missingPassword'); 78 | } 79 | 80 | @BeforeCreate 81 | static async hashPassword (user: User, options: any) { 82 | if (!options.transaction) throw new Error('Missing transaction.'); 83 | 84 | user.password = crypto.createHmac('sha256', user.password).digest('hex'); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/modules/common/models/interfaces/IAction.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { Instance } from 'sequelize'; 4 | 5 | export interface IAction { 6 | id?: number; 7 | apiAiUserId: number; 8 | agentName: string; 9 | actionName: string; 10 | requestId: string; 11 | data: any; 12 | response: any; 13 | createdAt?: Date; 14 | updatedAt?: Date; 15 | deletedAt?: Date; 16 | } 17 | 18 | export interface IActionInstance extends Instance { 19 | dataValues: IAction; 20 | } 21 | -------------------------------------------------------------------------------- /src/modules/common/models/interfaces/IApiAiUser.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { Instance } from 'sequelize'; 4 | 5 | export interface IApiAiUser { 6 | id?: number; 7 | firstName: string; 8 | lastName: string; 9 | email: string; 10 | accessToken: string; 11 | googleUserId: string; 12 | createdAt?: Date; 13 | updatedAt?: Date; 14 | deletedAt?: Date; 15 | } 16 | 17 | export interface IApiAiUserInstance extends Instance { 18 | dataValues: IApiAiUser; 19 | } 20 | -------------------------------------------------------------------------------- /src/modules/common/models/interfaces/IUser.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { Instance } from 'sequelize'; 4 | 5 | export interface IUser { 6 | id?: number; 7 | firstName: string; 8 | lastName: string; 9 | email: string; 10 | password: string; 11 | birthday?: Date; 12 | createdAt?: Date; 13 | updatedAt?: Date; 14 | deletedAt?: Date; 15 | } 16 | 17 | export interface IUserInstance extends Instance { 18 | dataValues: IUser; 19 | } 20 | -------------------------------------------------------------------------------- /src/modules/localTunnel/interfaces/ILocalTunnelService.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | export interface ILocalTunnelService { 4 | readonly url: string; 5 | 6 | /** 7 | * @description: Allow to start a new local tunnel and set the url. 8 | * @return {Promise} 9 | */ 10 | start (): Promise; 11 | 12 | /** 13 | * @description: Allow to close an existing local tunnel. 14 | */ 15 | close (): void; 16 | } 17 | -------------------------------------------------------------------------------- /src/modules/localTunnel/localTunnel.controller.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { Controller, Get, HttpStatus, Request, Response } from '@nestjs/common'; 4 | import { LocalTunnelService } from './localTunnel.service'; 5 | 6 | @Controller() 7 | export class LocalTunnelController { 8 | constructor (private localTunnelService: LocalTunnelService) { } 9 | 10 | @Get('localTunnelStart') 11 | public async localTunnelStart (@Request() req, @Response() res) { 12 | await this.localTunnelService.start(); 13 | return res.status(HttpStatus.OK).send(`Local tunnel started on ${this.localTunnelService.url}.`); 14 | } 15 | 16 | @Get('localTunnelStop') 17 | public async localTunnelStop (@Request() req, @Response() res) { 18 | this.localTunnelService.close(); 19 | return res.status(HttpStatus.OK).send('Local tunnel stopped.'); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/modules/localTunnel/localTunnel.module.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { Module } from '@nestjs/common'; 4 | import { MiddlewaresConsumer } from '@nestjs/common/interfaces/middlewares'; 5 | import { AuthMiddleware } from '../common/index'; 6 | import { LocalTunnelService } from '../localTunnel/localTunnel.service'; 7 | import { LocalTunnelController } from '../localTunnel/localTunnel.controller'; 8 | 9 | @Module({ 10 | controllers: [LocalTunnelController], 11 | components: [LocalTunnelService], 12 | modules: [], 13 | exports: [] 14 | }) 15 | export class LocalTunnelModule { 16 | configure (consumer: MiddlewaresConsumer) { 17 | consumer.apply(AuthMiddleware).forRoutes(LocalTunnelController); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/modules/localTunnel/localTunnel.service.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { Component } from '@nestjs/common'; 4 | import * as localTunnel from 'localtunnel'; 5 | import { config } from '../common/index'; 6 | import { ILocalTunnelService } from './interfaces/ILocalTunnelService'; 7 | 8 | @Component() 9 | export class LocalTunnelService implements ILocalTunnelService { 10 | private _localTunnel: any; 11 | private _url: string; 12 | 13 | constructor () { 14 | this._localTunnel = null; 15 | this._url = ''; 16 | } 17 | 18 | get url (): string { 19 | return this._url; 20 | } 21 | 22 | start (): Promise { 23 | return new Promise((resolve, reject) => { 24 | if (!this._url) { 25 | localTunnel(config.localTunnelPort, (err, tunnel) => { 26 | if (err) throw new Error(err); 27 | 28 | this._localTunnel = tunnel; 29 | this._url = tunnel.url; 30 | console.log(`local tunnel started on ${this._url}`); 31 | return resolve(`local tunnel started on ${this._url}`); 32 | }); 33 | } else { 34 | return reject(`local tunnel already started on ${this._url}`); 35 | } 36 | }); 37 | } 38 | 39 | close (): void { 40 | this._url = ''; 41 | this._localTunnel && this._localTunnel.close(); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/modules/users/users.controller.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { Controller, Get, Post, Put, Delete, HttpStatus, Request, Response } from '@nestjs/common'; 4 | import { MessageCodeError, sequelize, User } from '../common/index'; 5 | 6 | @Controller() 7 | export class UsersController { 8 | @Get('users') 9 | public async index (@Request() req, @Response() res) { 10 | const users = await User.findAll(); 11 | return res.status(HttpStatus.OK).json(users); 12 | } 13 | 14 | @Post('users') 15 | public async create (@Request() req, @Response() res) { 16 | const body = req.body; 17 | if (!body || (body && Object.keys(body).length === 0)) throw new MessageCodeError('user:create:missingInformation'); 18 | 19 | await sequelize.transaction(async t => { 20 | return await User.create(body, { transaction: t }); 21 | }); 22 | 23 | return res.status(HttpStatus.CREATED).send(); 24 | } 25 | 26 | @Get('users/:id') 27 | public async show (@Request() req, @Response() res) { 28 | const id = req.params.id; 29 | if (!id) throw new MessageCodeError('user:show:missingId'); 30 | 31 | const user = await User.findOne({ 32 | where: { id } 33 | }); 34 | return res.status(HttpStatus.OK).json(user); 35 | } 36 | 37 | @Put('users/:id') 38 | public async update (@Request() req, @Response() res) { 39 | const id = req.params.id; 40 | const body = req.body; 41 | if (!id) throw new MessageCodeError('user:update:missingId'); 42 | if (!body || (body && Object.keys(body).length === 0)) throw new MessageCodeError('user:update:missingInformation'); 43 | 44 | await sequelize.transaction(async t => { 45 | const user = await User.findById(id, { transaction: t }); 46 | if (!user) throw new MessageCodeError('user:notFound'); 47 | 48 | /* Keep only the values which was modified. */ 49 | const newValues = {}; 50 | for (const key of Object.keys(body)) { 51 | if (user[key] !== body[key]) newValues[key] = body[key]; 52 | } 53 | 54 | return await user.update(newValues, { transaction: t }); 55 | }); 56 | 57 | return res.status(HttpStatus.OK).send(); 58 | } 59 | 60 | @Delete('users/:id') 61 | public async delete (@Request() req, @Response() res) { 62 | const id = req.params.id; 63 | if (!id) throw new MessageCodeError('user:delete:missingId'); 64 | 65 | await 66 | sequelize.transaction(async t => { 67 | return await User.destroy({ 68 | where: { id }, 69 | transaction: t 70 | }); 71 | }); 72 | 73 | return res.status(HttpStatus.OK).send(); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/modules/users/users.module.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { Module, RequestMethod } from '@nestjs/common'; 4 | import { MiddlewaresConsumer } from '@nestjs/common/interfaces/middlewares'; 5 | import { AuthMiddleware } from '../common/index'; 6 | import { UsersController } from './users.controller'; 7 | 8 | @Module({ 9 | controllers: [UsersController], 10 | components: [], 11 | modules: [], 12 | exports: [] 13 | }) 14 | export class UsersModule { 15 | configure (consumer: MiddlewaresConsumer) { 16 | consumer.apply(AuthMiddleware).forRoutes( 17 | { path: '/users', method: RequestMethod.GET }, 18 | { path: '/users/:id', method: RequestMethod.GET }, 19 | { path: '/users/:id', method: RequestMethod.PUT }, 20 | { path: '/users/:id', method: RequestMethod.DELETE } 21 | ); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/server.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('dotenv').config(); 4 | 5 | import { NestFactory } from '@nestjs/core'; 6 | import * as express from 'express'; 7 | import * as bodyParser from 'body-parser'; 8 | import { DispatchError } from './modules/common/filters/DispatchError'; 9 | import { ApplicationModule } from './modules/app.module'; 10 | 11 | const instance = express(); 12 | /* Express middleware. */ 13 | instance.use(bodyParser.json()); 14 | instance.use(bodyParser.urlencoded({ extended: false })); 15 | /* End of express middleware. */ 16 | 17 | async function bootstrap (): Promise { 18 | const app = await NestFactory.create(ApplicationModule, instance); 19 | /* App filters. */ 20 | app.useGlobalFilters(new DispatchError()); 21 | /* End of app filters. */ 22 | await app.listen(3000); 23 | } 24 | 25 | bootstrap().then(() => console.log('Application is listening on port 3000.')); 26 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es2017", 5 | "declaration": false, 6 | "noImplicitAny": false, 7 | "removeComments": false, 8 | "strictNullChecks": true, 9 | "noLib": false, 10 | "emitDecoratorMetadata": true, 11 | "experimentalDecorators": true, 12 | "sourceMap": true, 13 | "outDir": "build" 14 | }, 15 | "include": [ 16 | "migrate.ts", 17 | "src/server.ts", 18 | "src/config/**/*.ts", 19 | "src/filters/**/*.ts", 20 | "src/lib/**/*.ts", 21 | "src/middlewares/**/*.ts", 22 | "src/migrations/**/*.ts", 23 | "src/models/**/*.ts", 24 | "src/modules/**/*.ts", 25 | "src/modules/auth/tests/**/*.ts" 26 | ], 27 | "exclude": [ 28 | "node_modules", 29 | "assets" 30 | ] 31 | } -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tslint-config-standard", 3 | "rules": { 4 | "await-promise": false, 5 | "ter-indent": [true, 4], 6 | "semicolon": [true, "always"] 7 | } 8 | } --------------------------------------------------------------------------------