├── .gitignore ├── LICENSE ├── README.md ├── __tests__ ├── dogs.test.js └── setupTests.js ├── docs ├── _config.yml ├── _sass │ ├── color_schemes │ │ └── !dark.scss │ └── custom │ │ └── !custom.scss ├── advance-guide │ ├── cronjobs.md │ ├── index.md │ ├── seeds.md │ ├── sockets.md │ └── swagger.md ├── basic-guide │ ├── business.md │ ├── controllers.md │ ├── index.md │ ├── models.md │ └── routes.md ├── custom-libs │ ├── aws.md │ ├── fcm.md │ └── index.md ├── environments │ └── index.md ├── getting-started │ ├── folder-structure.md │ ├── index.md │ └── installation.md ├── index.md ├── scripts │ └── index.md └── unit-tests │ └── index.md ├── jsconfig.json ├── package.json ├── pm2.json ├── src ├── app.js ├── business │ ├── .gitkeep │ ├── auth.business.js │ └── dogs.business.js ├── constants │ └── config.constant.js ├── controllers │ ├── .gitkeep │ ├── auth.controller.js │ └── dogs.controller.js ├── cronjobs │ └── .gitkeep ├── index.js ├── layouts │ ├── email.recover.hbs │ ├── email.register.hbs │ ├── favicon.png │ ├── index.css │ ├── index.hbs │ └── logo.gif ├── libs │ ├── express.lib.js │ ├── mongoose.lib.js │ ├── redis.lib.js │ ├── socketio.lib.js │ └── swagger.lib.js ├── models │ ├── .gitkeep │ ├── dogs.model.js │ └── user.model.js ├── routes │ ├── .gitkeep │ ├── auth.route.js │ └── dogs.route.js ├── seeds │ ├── !dogs.seed.js │ ├── !user.seed.js │ └── .gitkeep ├── sockets │ ├── .gitkeep │ └── dogs.socket.js └── utils │ ├── auth.util.js │ ├── autoload.util.js │ ├── helper.util.js │ ├── layout.util.js │ ├── middleware.util.js │ ├── seed.util.js │ └── swagger.util.js ├── webpack.config.babel.js └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | backups 4 | .DS_Store 5 | .env 6 | .env.test 7 | .env.production 8 | .env.local 9 | .env.development.local 10 | .env.test.local 11 | .env.production.local -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 leonardo Rico 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 | # NODETOMIC 2 | 3 | Minimalist boilerplate for **nodejs**, designed for vertical and horizontal scalability. 4 | 5 | ## Technologies 6 | 7 | - Express 8 | - Redis 6 9 | - MongoDB 4 10 | - Swagger 3 11 | - Webpack 5 12 | - Babel 7 13 | - Socket 4 14 | - Eslint 15 | - Prettier 16 | - Jest 17 | 18 | ## Installation 19 | 20 | ```bash 21 | git clone https://github.com/kevoj/nodetomic 22 | cd nodetomic 23 | yarn 24 | ``` 25 | 26 | Then, you will need to create a .env file in the root of the project 27 | 28 | ```bash 29 | PROJECT_MODE=development 30 | PROJECT_NAME=example-name 31 | SERVER_HOSTNAME=localhost 32 | SERVER_PORT=8000 33 | SERVER_WEBSOCKET_PORT=8001 34 | SWAGGER_HOSTNAME=localhost 35 | SWAGGER_API_DOCS=true 36 | JWT_SECRET_KEY=shhhh 37 | MONGODB_HOSTNAME=127.0.0.1 38 | MONGODB_PORT=27017 39 | MONGODB_DATABASE=example-dev 40 | MONGODB_USERNAME= 41 | MONGODB_PASSWORD= 42 | REDIS_HOSTNAME=127.0.0.1 43 | REDIS_PORT=6379 44 | REDIS_PASSWORD= 45 | ``` 46 | ## Scripts 47 | 48 | ### start 49 | 50 | Start the project in development mode with the .env file that is in the root 51 | 52 | ```bash 53 | yarn start 54 | ``` 55 | 56 | ### test 57 | 58 | Run the unit tests 59 | 60 | ```bash 61 | yarn test 62 | ``` 63 | 64 | ### build 65 | 66 | Compile the project 67 | 68 | ```bash 69 | yarn build 70 | ``` 71 | 72 | ## Docs 73 | 74 | [Guide](https://kevoj.github.io/nodetomic) 75 | 76 | ## API docs 77 | 78 | ![image](https://user-images.githubusercontent.com/2652129/128109277-2a7bed2d-f6e7-4fe8-8e67-215fbf60f186.png) 79 | 80 | ## Scalability 81 | 82 | ### Starting point 83 | 84 | ![nodetomic_1](https://user-images.githubusercontent.com/2652129/128117943-ba569149-8f3c-4252-9231-9e16936167a2.png) 85 | 86 | ### cluster mode **(NO SHARED STATE)** 87 | 88 | ![nodetomic_2](https://user-images.githubusercontent.com/2652129/128117945-cd4abb81-7c36-4cc3-8de8-0f8b809c6988.png) 89 | 90 | ### Add Redis to shared state 91 | 92 | ![nodetomic_3](https://user-images.githubusercontent.com/2652129/128117950-b576e53a-d14b-4b7c-96cc-c317958c1bd3.png) 93 | 94 | ### Added multiple servers and pm2 load balancing 95 | 96 | ![nodetomic_4](https://user-images.githubusercontent.com/2652129/128117954-be4c1813-5222-474c-bac1-40ffd6aace60.png) 97 | 98 | ### Added database and load balancer 99 | 100 | ![nodetomic_5](https://user-images.githubusercontent.com/2652129/128117959-e2893fb2-7588-4fb0-8625-b237be20dad2.png) 101 | 102 | ### Added redis cluster and mongodb sharded clusters 103 | 104 | ![nodetomic_6](https://user-images.githubusercontent.com/2652129/128117966-7bbc6054-97a7-4ae4-bfc1-71071c41fdd7.png) 105 | 106 | ### Conclusion 107 | 108 | ![nodetomic_7](https://user-images.githubusercontent.com/2652129/128117968-de8d3d3f-25af-4b5f-bfab-cac9d9e9dac9.png) 109 | -------------------------------------------------------------------------------- /__tests__/dogs.test.js: -------------------------------------------------------------------------------- 1 | // Libs 2 | import { create } from 'apisauce'; 3 | 4 | const api = create({ 5 | baseURL: host 6 | }); 7 | 8 | describe('Dogs', () => { 9 | test('all dogs', async () => { 10 | const { status, data } = await api.get('/api/dogs/all'); 11 | expect(200).toBe(status); 12 | expect([]).toEqual(data); 13 | }); 14 | 15 | test('all dogs - logged', async () => { 16 | const { status, data } = await api.get('/api/dogs/all/logged'); 17 | expect(401).toBe(status); 18 | expect('unauthorized').toEqual(data?.result); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /__tests__/setupTests.js: -------------------------------------------------------------------------------- 1 | import { SERVER_HOSTNAME, SERVER_PORT } from '@/constants/config.constant'; 2 | 3 | global.host = `http://${SERVER_HOSTNAME}:${SERVER_PORT}`; 4 | -------------------------------------------------------------------------------- /docs/_config.yml: -------------------------------------------------------------------------------- 1 | title: Nodetomic 2 | description: Nodetomic documentation 3 | baseurl: '/nodetomic' # the subpath of your site, e.g. /blog 4 | url: 'https://kevoj.github.io' 5 | remote_theme: pmarsceill/just-the-docs 6 | color_scheme: light 7 | plugins: 8 | - jekyll-relative-links 9 | - jekyll-default-layout 10 | relative_links: 11 | enabled: true 12 | collections: true 13 | aux_links: 14 | 'Nodetomic on GitHub': 15 | - '//github.com/kevoj/nodetomic' 16 | -------------------------------------------------------------------------------- /docs/_sass/color_schemes/!dark.scss: -------------------------------------------------------------------------------- 1 | $code-background-color: #282c34; 2 | -------------------------------------------------------------------------------- /docs/_sass/custom/!custom.scss: -------------------------------------------------------------------------------- 1 | $dark: #282c34; 2 | $red: #e06c75; 3 | $green: #98c379; 4 | $orange: #e5c07b; 5 | $blue: #61afef; 6 | $magenta: #c678dd; 7 | $cyan: #56b6c2; 8 | $white: #fff; 9 | $grey: #abb2bf; 10 | $grey-2: #93a1a1; 11 | $grey-3: #586e75; 12 | 13 | code { 14 | color: $white; 15 | } 16 | 17 | .highlight .c { 18 | color: $grey-3; 19 | } // comment // 20 | .highlight .err { 21 | color: $grey-2; 22 | } // error // 23 | .highlight .g { 24 | color: $grey-2; 25 | } // generic // 26 | .highlight .k { 27 | color: $magenta; 28 | } // keyword // 29 | .highlight .l { 30 | color: $grey-2; 31 | } // literal // 32 | .highlight .n { 33 | color: $grey-2; 34 | } // name // 35 | .highlight .o { 36 | color: $magenta; 37 | } // operator // 38 | .highlight .x { 39 | color: $red; 40 | } // other // 41 | .highlight .p { 42 | color: $grey-2; 43 | } // punctuation // 44 | .highlight .cm { 45 | color: $grey-3; 46 | } // comment.multiline // 47 | .highlight .cp { 48 | color: $magenta; 49 | } // comment.preproc // 50 | .highlight .c1 { 51 | color: $grey-3; 52 | } // comment.single // 53 | .highlight .cs { 54 | color: $magenta; 55 | } // comment.special // 56 | .highlight .gd { 57 | color: $green; 58 | } // generic.deleted // 59 | .highlight .ge { 60 | font-style: italic; 61 | color: $grey-2; 62 | } // generic.emph // 63 | .highlight .gr { 64 | color: $red; 65 | } // generic.error // 66 | .highlight .gh { 67 | color: $red; 68 | } // generic.heading // 69 | .highlight .gi { 70 | color: $magenta; 71 | } // generic.inserted // 72 | .highlight .go { 73 | color: $grey-2; 74 | } // generic.output // 75 | .highlight .gp { 76 | color: $grey-2; 77 | } // generic.prompt // 78 | .highlight .gs { 79 | font-weight: bold; 80 | color: $grey-2; 81 | } // generic.strong // 82 | .highlight .gu { 83 | color: $red; 84 | } // generic.subheading // 85 | .highlight .gt { 86 | color: $grey-2; 87 | } // generic.traceback // 88 | .highlight .kc { 89 | color: $red; 90 | } // keyword.constant // 91 | .highlight .kd { 92 | color: $magenta; 93 | } // keyword.declaration // 94 | .highlight .kn { 95 | color: $magenta; 96 | } // keyword.namespace // 97 | .highlight .kp { 98 | color: $magenta; 99 | } // keyword.pseudo // 100 | .highlight .kr { 101 | color: $blue; 102 | } // keyword.reserved // 103 | .highlight .kt { 104 | color: $red; 105 | } // keyword.type // 106 | .highlight .ld { 107 | color: $grey-2; 108 | } // literal.date // 109 | .highlight .m { 110 | color: $green; 111 | } // literal.number // 112 | .highlight .s { 113 | color: $green; 114 | } // literal.string // 115 | .highlight .na { 116 | color: $green; 117 | } // name.attribute // 118 | .highlight .nb { 119 | color: $orange; 120 | } // name.builtin // 121 | .highlight .nc { 122 | color: $blue; 123 | } // name.class // 124 | .highlight .no { 125 | color: $red; 126 | } // name.constant // 127 | .highlight .nd { 128 | color: $blue; 129 | } // name.decorator // 130 | .highlight .ni { 131 | color: $red; 132 | } // name.entity // 133 | .highlight .ne { 134 | color: $red; 135 | } // name.exception // 136 | .highlight .nf { 137 | color: $blue; 138 | } // name.function // 139 | .highlight .nl { 140 | color: $grey; 141 | } // name.label // 142 | .highlight .nn { 143 | color: $grey-2; 144 | } // name.namespace // 145 | .highlight .nx { 146 | color: $grey; 147 | } // name.other // 148 | .highlight .py { 149 | color: $grey-2; 150 | } // name.property // 151 | .highlight .nt { 152 | color: $blue; 153 | } // name.tag // 154 | .highlight .nv { 155 | color: $blue; 156 | } // name.variable // 157 | .highlight .ow { 158 | color: $magenta; 159 | } // operator.word // 160 | .highlight .w { 161 | color: $grey-2; 162 | } // text.whitespace // 163 | .highlight .mf { 164 | color: $green; 165 | } // literal.number.float // 166 | .highlight .mh { 167 | color: $green; 168 | } // literal.number.hex // 169 | .highlight .mi { 170 | color: $green; 171 | } // literal.number.integer // 172 | .highlight .mo { 173 | color: $green; 174 | } // literal.number.oct // 175 | .highlight .sb { 176 | color: $grey-3; 177 | } // literal.string.backtick // 178 | .highlight .sc { 179 | color: $green; 180 | } // literal.string.char // 181 | .highlight .sd { 182 | color: $grey-2; 183 | } // literal.string.doc // 184 | .highlight .s2 { 185 | color: $green; 186 | } // literal.string.double // 187 | .highlight .se { 188 | color: $red; 189 | } // literal.string.escape // 190 | .highlight .sh { 191 | color: $grey-2; 192 | } // literal.string.heredoc // 193 | .highlight .si { 194 | color: $green; 195 | } // literal.string.interpol // 196 | .highlight .sx { 197 | color: $green; 198 | } // literal.string.other // 199 | .highlight .sr { 200 | color: $red; 201 | } // literal.string.regex // 202 | .highlight .s1 { 203 | color: $green; 204 | } // literal.string.single // 205 | .highlight .ss { 206 | color: $green; 207 | } // literal.string.symbol // 208 | .highlight .bp { 209 | color: $blue; 210 | } // name.builtin.pseudo // 211 | .highlight .vc { 212 | color: $blue; 213 | } // name.variable.class // 214 | .highlight .vg { 215 | color: $blue; 216 | } // name.variable.global // 217 | .highlight .vi { 218 | color: $blue; 219 | } // name.variable.instance // 220 | .highlight .il { 221 | color: $green; 222 | } // literal.number.integer.long // 223 | .highlight .dl { 224 | color: $green; 225 | } // simple quotes // 226 | -------------------------------------------------------------------------------- /docs/advance-guide/cronjobs.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Cronjobs 3 | parent: Advance Guide 4 | has_children: false 5 | nav_order: 3 6 | --- 7 | 8 | # Cronjobs 9 | 10 | ```javascript 11 | // Models 12 | import DogsBusiness from '@/business/dogs.business'; 13 | // Libs 14 | import cron from 'node-cron'; 15 | 16 | // Execute At minute 7 past every 6th hour. 17 | cron.schedule('07 */6 * * *', async () => { 18 | await DogsBusiness.getAll(); 19 | }); 20 | ``` 21 | 22 | # Tips 23 | 24 | ```javascript 25 | // Run script only on specific PM2 instance 26 | if (process.env.NODE_APP_INSTANCE === '0') { 27 | console.log('hello world'); 28 | } 29 | ``` 30 | -------------------------------------------------------------------------------- /docs/advance-guide/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Advance Guide 3 | has_children: true 4 | nav_order: 4 5 | --- 6 | 7 | ## Advance Guide 8 | -------------------------------------------------------------------------------- /docs/advance-guide/seeds.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Seeds 3 | parent: Advance Guide 4 | has_children: false 5 | nav_order: 2 6 | --- 7 | 8 | # Seeds 9 | 10 | ```javascript 11 | // Models 12 | import DogsModel from '@/models/dogs.model'; 13 | // Utils 14 | import { insert } from '@/utils/seed.util'; 15 | // Data 16 | const data = [ 17 | { 18 | name: 'Sparky', 19 | race: 'Beagle', 20 | user_id: '6108db02bb8ea9e69b2984a2' 21 | }, 22 | { 23 | name: 'Zeus', 24 | race: 'Chihuahua', 25 | user_id: '6108db02bb8ea9e69b2984a2' 26 | }, 27 | { 28 | name: 'Poseidon', 29 | race: 'Bulldog', 30 | user_id: '6108db1b8d75624c1dd2ba2f' 31 | } 32 | ]; 33 | 34 | export default async () => await insert(DogsModel, data); 35 | ``` 36 | -------------------------------------------------------------------------------- /docs/advance-guide/sockets.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Sockets 3 | parent: Advance Guide 4 | has_children: false 5 | nav_order: 1 6 | --- 7 | 8 | # Sockets 9 | 10 | ```javascript 11 | // Vars 12 | let socket = null; 13 | let io = null; 14 | 15 | // Constructor 16 | export default (_socket, _io) => { 17 | socket = _socket; 18 | io = _io; 19 | on(); 20 | }; 21 | 22 | // Listen events 23 | const on = () => { 24 | socket.on('dogs:ping', (data) => { 25 | io.emit('dogs:pong', data); 26 | }); 27 | }; 28 | 29 | export { socket, io }; 30 | ``` 31 | -------------------------------------------------------------------------------- /docs/advance-guide/swagger.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Swagger 3 | parent: Advance Guide 4 | has_children: false 5 | nav_order: 4 6 | --- 7 | 8 | # Swagger 9 | 10 | ```javascript 11 | // Body Parameter 12 | 13 | /** 14 | * POST /api/dogs/create 15 | * @summary get dogs 16 | * @tags Dogs 17 | * @param {string} name.form.required - dog name 18 | * @return {object} 200 - Success 19 | * @return {object} 5XX - Error 20 | */ 21 | 22 | // Path Parameter 23 | 24 | /** 25 | * GET /api/dogs/{dogId} 26 | * @summary get dog by id 27 | * @tags Dogs 28 | * @param {string} dogId.path.required - dogId 29 | * @return {object} 200 - Success 30 | * @return {object} 5XX - Error 31 | */ 32 | 33 | // Query Parameter 34 | 35 | /** 36 | * GET /api/dogs/filter?status=available 37 | * @summary get dogs by query 38 | * @tags Dogs 39 | * @param {string} status.query.required - status 40 | * @return {object} 200 - Success 41 | * @return {object} 5XX - Error 42 | */ 43 | 44 | // Header Parameter 45 | /** 46 | * GET /api/dogs 47 | * @summary with header param 48 | * @tags Dogs 49 | * @param {object} headerId.header.required - headerId 50 | * @return {object} 200 - Success 51 | * @return {object} 5XX - Error 52 | */ 53 | 54 | /** 55 | * Code Status 56 | * GET /api/dogs 57 | * @tags Dogs 58 | * @return {object} 200 - Success 59 | * @return {object} 400 - Bad request 60 | * @return {object} 401 - Unauthorized 61 | * @return {object} 403 - Forbidden 62 | * @return {object} 404 - Not found 63 | * @return {object} 405 - Unsupported action 64 | * @return {object} 422 - Invalid 65 | * @return {object} 5XX - Error 66 | */ 67 | ``` 68 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /docs/basic-guide/business.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Business 3 | parent: Basic Guide 4 | has_children: false 5 | nav_order: 2 6 | --- 7 | 8 | # Business 9 | 10 | ```javascript 11 | // Models 12 | import DogsModel from '@/models/dogs.model'; 13 | 14 | const getAll = async () => { 15 | // Database query 16 | return await DogsModel.find({}); 17 | }; 18 | 19 | export default { 20 | getAll 21 | }; 22 | ``` 23 | -------------------------------------------------------------------------------- /docs/basic-guide/controllers.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Controllers 3 | parent: Basic Guide 4 | has_children: false 5 | nav_order: 3 6 | --- 7 | 8 | # Controllers 9 | 10 | ```javascript 11 | // Business 12 | import DogBusiness from '@/business/dogs.business'; 13 | import { success, error } from '@/utils/helper.util'; 14 | // Libs 15 | import validator from 'validator'; 16 | 17 | const getAll = async (req, res) => { 18 | try { 19 | // Business logic 20 | const data = await DogBusiness.getAll(); 21 | // Return success 22 | success(res, data); 23 | } catch (err) { 24 | // Return error (if any) 25 | error(res, err); 26 | } 27 | }; 28 | 29 | export default { getAll }; 30 | ``` 31 | -------------------------------------------------------------------------------- /docs/basic-guide/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Basic Guide 3 | has_children: true 4 | nav_order: 3 5 | --- 6 | 7 | ## Basic Guide 8 | -------------------------------------------------------------------------------- /docs/basic-guide/models.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Models 3 | parent: Basic Guide 4 | has_children: false 5 | nav_order: 1 6 | --- 7 | 8 | # Models 9 | 10 | ```javascript 11 | import { Schema, model } from 'mongoose'; 12 | 13 | // Schema 14 | const schema = new Schema({ 15 | name: { 16 | type: String, 17 | default: null 18 | }, 19 | race: { 20 | type: String, 21 | default: null 22 | }, 23 | user_id: { 24 | type: Schema.Types.ObjectId, 25 | default: null 26 | }, 27 | created_at: { 28 | type: Date, 29 | default: Date.now 30 | } 31 | }); 32 | 33 | const Model = model('Dog', schema); 34 | 35 | export default Model; 36 | ``` 37 | -------------------------------------------------------------------------------- /docs/basic-guide/routes.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Routes 3 | parent: Basic Guide 4 | has_children: false 5 | nav_order: 4 6 | --- 7 | 8 | # Routes 9 | 10 | ```javascript 11 | import express from 'express'; 12 | // Controllers 13 | import DogsController from '@/controllers/dogs.controller'; 14 | // Utils 15 | import { mw } from '@/utils/middleware.util'; 16 | // Constants 17 | const router = express.Router(); 18 | 19 | /** 20 | * GET /api/dogs/all 21 | * @summary Get all dogs 22 | * @tags Dogs 23 | * @return {object} 200 - Success 24 | * @return {object} 5XX - Error 25 | * @example response - 200 - success response example 26 | * [ 27 | * { 28 | * "_id":"60d200765299bd36806d8999", 29 | * "name":"Sparky", 30 | * "race":"Beagle", 31 | * "user_id": "6108db02bb8ea9e69b2984a2", 32 | * "created_at":"2021-06-22T15:23:34.521Z" 33 | * } 34 | * ] 35 | */ 36 | router.get('/api/dogs/all', DogsController.getAll); 37 | 38 | 39 | export default router; 40 | ``` 41 | -------------------------------------------------------------------------------- /docs/custom-libs/aws.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: AWS 3 | parent: Custom libs 4 | nav_order: 1 5 | --- 6 | 7 | # AWS 8 | 9 | ### Install 10 | 11 | ```bash 12 | yarn add aws-sdk 13 | ``` 14 | 15 | ### Setup 16 | 17 | ```javascript 18 | import { 19 | AWS_ACCESS_KEY_ID, 20 | AWS_SECRET_ACCESS_KEY, 21 | AWS_REGION, 22 | AWS_PINPOINT_ID, 23 | PROJECT_MODE 24 | } from '@/constants/config.constant'; 25 | 26 | import { renderTemplate } from '../utils/layout.util'; 27 | 28 | AWS.config.update({ 29 | accessKeyId: AWS_ACCESS_KEY_ID, 30 | secretAccessKey: AWS_SECRET_ACCESS_KEY, 31 | region: AWS_REGION 32 | }); 33 | 34 | const aws_s3 = new AWS.S3(); 35 | const pinpoint = new AWS.Pinpoint({ apiVersion: '2016-12-01' }); 36 | ``` 37 | 38 | ### s3 39 | 40 | ```javascript 41 | const s3Upload = async (config = {}, files) => { 42 | try { 43 | console.log('s3 upload', { config }); 44 | let result = []; 45 | 46 | if (files && files instanceof Array) { 47 | let promises = []; 48 | 49 | for (const file of files) { 50 | promises.push( 51 | aws_s3 52 | .upload({ 53 | Bucket: config.bucket, 54 | Body: file.data, 55 | Key: `${PROJECT_MODE}${config.path}${file.md5}_${file.name}` 56 | }) 57 | .promise() 58 | ); 59 | } 60 | 61 | const values = await Promise.all(promises); 62 | 63 | values.map((x) => { 64 | result.push({ 65 | url: x.Location, 66 | ...config 67 | }); 68 | }); 69 | } 70 | 71 | console.log({ result }); 72 | return result; 73 | } catch (err) { 74 | console.log({ s3UploadError: err }); 75 | } 76 | }; 77 | ``` 78 | 79 | ```javascript 80 | const s3Delete = async (deletion) => { 81 | try { 82 | console.log('s3 delete', { deletion }); 83 | let result = []; 84 | 85 | // Always receive Array, even for 1 element 86 | if (deletion && deletion instanceof Array) { 87 | let promises = []; 88 | 89 | for (const element of deletion) { 90 | promises.push( 91 | aws_s3 92 | .deleteObject({ 93 | Bucket: element.bucket, 94 | Key: element.key 95 | }) 96 | .promise() 97 | ); 98 | } 99 | 100 | await Promise.all(promises); 101 | } 102 | 103 | result = deletion.map((x) => x.key); 104 | console.log(result); 105 | return result; 106 | } catch (err) { 107 | console.log({ s3DeleteError: err }); 108 | } 109 | }; 110 | ``` 111 | 112 | ### Pinpoint 113 | 114 | ```javascript 115 | const sendEmail = async (data) => { 116 | try { 117 | console.log('📧 email', data); 118 | 119 | if (PROJECT_MODE !== 'development') { 120 | const parts = {}; 121 | 122 | // simple email 123 | if (data.message) 124 | parts.TextPart = { 125 | Charset: 'UTF-8', 126 | Data: data.message 127 | }; 128 | 129 | // html email 130 | if (data.template) 131 | parts.HtmlPart = { 132 | Charset: 'UTF-8', 133 | Data: await renderTemplate(data.template, data.params) 134 | }; 135 | 136 | const params = { 137 | ApplicationId: AWS_PINPOINT_ID, 138 | MessageRequest: { 139 | Addresses: { 140 | [data.to]: { 141 | ChannelType: 'EMAIL' 142 | } 143 | }, 144 | MessageConfiguration: { 145 | EmailMessage: { 146 | FromAddress: data.from, 147 | SimpleEmail: { 148 | Subject: { 149 | Charset: 'UTF-8', 150 | Data: data.subject 151 | }, 152 | ...parts 153 | } 154 | } 155 | } 156 | } 157 | }; 158 | 159 | const result = await pinpoint.sendMessages(params).promise(); 160 | console.log({ result }); 161 | console.log({ detail: result.MessageResponse.Result }); 162 | } 163 | } catch (err) { 164 | console.log({ sendEmailError: err }); 165 | } 166 | }; 167 | ``` 168 | 169 | ```javascript 170 | const sendSMS = async (data) => { 171 | try { 172 | console.log('📱 sms', data); 173 | 174 | if (PROJECT_MODE !== 'development') { 175 | const params = { 176 | ApplicationId: AWS_PINPOINT_ID, 177 | MessageRequest: { 178 | Addresses: { 179 | [data.to]: { 180 | BodyOverride: data.message, 181 | ChannelType: 'SMS' 182 | } 183 | }, 184 | MessageConfiguration: { 185 | SMSMessage: { 186 | Body: data.message, 187 | MessageType: 'TRANSACTIONAL' 188 | } 189 | } 190 | } 191 | }; 192 | 193 | const result = await pinpoint.sendMessages(params).promise(); 194 | console.log({ result }); 195 | } 196 | } catch (err) { 197 | console.log({ sendSMSError: err }); 198 | } 199 | }; 200 | ``` 201 | 202 | ### TEST 203 | 204 | ```javascript 205 | // Test S3 upload 206 | s3Upload( 207 | { 208 | bucket: 'Nodetomic', 209 | path: `development/images/user1/` 210 | }, 211 | files 212 | ); 213 | 214 | // Test S3 delete 215 | s3Delete([ 216 | { 217 | bucket: 'Nodetomic', 218 | key: `development/images/xxxxx.jpg` 219 | } 220 | ]); 221 | 222 | // Test SMS 223 | sendSMS({ 224 | to: '+573001111111', 225 | from: 'Nodetomic', 226 | message: 'Nodetomic: 123' 227 | }); 228 | 229 | // Test Email 230 | sendEmail({ 231 | to: 'user@example.com', 232 | from: 'hi@example.com', 233 | subject: 'Nodetomic', 234 | message: 'This is a test', 235 | template: 'email.register', 236 | params: { 237 | code: 123 238 | } 239 | }); 240 | ``` 241 | -------------------------------------------------------------------------------- /docs/custom-libs/fcm.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: FCM 3 | parent: Custom libs 4 | nav_order: 2 5 | --- 6 | 7 | # FCM 8 | 9 | ### FCM 10 | 11 | ```bash 12 | yarn add fcm-push 13 | ``` 14 | 15 | ### Setup 16 | 17 | ```javascript 18 | // Libs 19 | import FCM from 'fcm-push'; 20 | // Constants 21 | import { FCM_SERVER_KEY, PROJECT_MODE } from '@/constants/config.constant'; 22 | // Business 23 | import UsersBusiness from '@/business/users.business'; 24 | 25 | const fcm = new FCM(FCM_SERVER_KEY); 26 | 27 | const sendPushNotification = async (body) => { 28 | try { 29 | if (body?.to.length === 0) { 30 | throw 'Empty push_notifications array...'; 31 | } 32 | 33 | const message = { 34 | // to: abcdf.... //One... required fill with device token or topics 35 | registration_ids: body.to, // Multiple...1 and at most 1000 registration tokens 36 | collapse_key: 'your_collapse_key', 37 | priority: 'high', 38 | notification: body.notification, 39 | data: body.data || {} 40 | }; 41 | 42 | console.log('🔔 Push notification', message); 43 | 44 | if (PROJECT_MODE !== 'development') { 45 | let result = await fcm.send(message); 46 | 47 | console.log({ result }); 48 | 49 | result = JSON.parse(result); 50 | 51 | const results = result?.results; 52 | 53 | if (results?.length > 0) { 54 | // Check results array, preserve tokens that were successful by reading their index in 'results' (same order as they came as argument in this function) 55 | const successTokens = body.to.filter((x, i) => !results[i].error); 56 | 57 | const oldTokensCount = body.to.length - successTokens.length || 0; 58 | 59 | console.log(`Old Tokens to delete: ${oldTokensCount}`); 60 | 61 | // Optional 62 | // if (oldTokensCount > 0) { 63 | // await UsersBusiness.fcmClearGarbage(body.userId, successTokens); 64 | // } 65 | } 66 | 67 | return result; 68 | } 69 | } catch (err) { 70 | console.log({ FCMError: err }); 71 | 72 | if (err === 'NotRegistered') { 73 | console.log( 74 | 'All Tokens failed, proceeding to empty push_notifications array for this User...' 75 | ); 76 | // Optional 77 | // await UsersBusiness.fcmClearGarbage(body.receiverId, []); 78 | } 79 | 80 | return { err, success: false }; 81 | } 82 | }; 83 | 84 | export { sendPushNotification }; 85 | ``` 86 | 87 | ### Remove invalid tokens from db (Optional) 88 | 89 | ```javascript 90 | const fcmClearGarbage = async (userId, tokens) => { 91 | await UserPROJECT_MODEl.findOneAndUpdate( 92 | { 93 | _id: mongoose.Types.ObjectId(userId) 94 | }, 95 | { 96 | push_notifications: tokens 97 | } 98 | ); 99 | }; 100 | ``` 101 | 102 | ### Update token (Optional) 103 | 104 | ```javascript 105 | const fcmUpdate = async (userId, token) => { 106 | return await UserPROJECT_MODEl.updateOne( 107 | { 108 | _id: mongoose.Types.ObjectId(userId), 109 | deleted_at: null 110 | }, 111 | { 112 | $addToSet: { 113 | push_notifications: token 114 | } 115 | } 116 | ); 117 | }; 118 | ``` 119 | 120 | ### TEST 121 | 122 | ```javascript 123 | Test FCM 124 | await sendPushNotification({ 125 | to: ["abc"], 126 | notification: { 127 | title: "Nodetomic", 128 | body: "Hello world", 129 | }, 130 | userId: "" 131 | }); 132 | ``` 133 | -------------------------------------------------------------------------------- /docs/custom-libs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Custom libs 3 | has_children: true 4 | nav_order: 5 5 | --- 6 | 7 | # Custom libs -------------------------------------------------------------------------------- /docs/environments/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Environments 3 | has_children: false 4 | nav_order: 7 5 | --- 6 | 7 | # Environments 8 | 9 | Create or replace **.env** file in the root of the project: 10 | 11 | ### Development 12 | 13 | ```bash 14 | PROJECT_MODE=development 15 | PROJECT_NAME=example-name 16 | SERVER_HOSTNAME=localhost 17 | SERVER_PORT=8000 18 | SERVER_WEBSOCKET_PORT=8001 19 | SWAGGER_HOSTNAME=localhost 20 | SWAGGER_API_DOCS=true 21 | JWT_SECRET_KEY=shhhh 22 | MONGODB_HOSTNAME=127.0.0.1 23 | MONGODB_PORT=27017 24 | MONGODB_DATABASE=example-dev 25 | MONGODB_USERNAME= 26 | MONGODB_PASSWORD= 27 | REDIS_HOSTNAME=127.0.0.1 28 | REDIS_PORT=6379 29 | REDIS_PASSWORD= 30 | ``` 31 | 32 | ### Testing 33 | 34 | ```bash 35 | PROJECT_MODE=testing 36 | PROJECT_NAME=example-name 37 | SERVER_HOSTNAME=localhost 38 | SERVER_PORT=9000 39 | SERVER_WEBSOCKET_PORT=9001 40 | SWAGGER_HOSTNAME=test-api.example.com 41 | SWAGGER_API_DOCS=true 42 | JWT_SECRET_KEY=test123 43 | MONGODB_HOSTNAME=mongo 44 | MONGODB_PORT=27017 45 | MONGODB_DATABASE=example-test 46 | MONGODB_USERNAME=test_user 47 | MONGODB_PASSWORD=6u5hWW8A4HBCbCUF 48 | REDIS_HOSTNAME=redis 49 | REDIS_PORT=6379 50 | REDIS_PASSWORD=CDEkW6jfPQ3rKSyK 51 | ``` 52 | 53 | ### Production 54 | 55 | ```bash 56 | PROJECT_MODE=production 57 | PROJECT_NAME=example-name 58 | SERVER_HOSTNAME=localhost 59 | SERVER_PORT=10000 60 | SERVER_WEBSOCKET_PORT=10001 61 | SWAGGER_HOSTNAME=prod-api.example.com 62 | SWAGGER_API_DOCS=false 63 | JWT_SECRET_KEY=prod123 64 | MONGODB_HOSTNAME=mongo 65 | MONGODB_PORT=27014 66 | MONGODB_DATABASE=example-prod 67 | MONGODB_USERNAME=prod_user 68 | MONGODB_PASSWORD=PzKypJp8VsUDF5gZ 69 | REDIS_HOSTNAME=redis 70 | REDIS_PORT=6374 71 | REDIS_PASSWORD=7pzMggTEw3MxQ76W 72 | ``` 73 | -------------------------------------------------------------------------------- /docs/getting-started/folder-structure.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Folder structure 3 | parent: Getting Started 4 | has_children: false 5 | nav_order: 2 6 | --- 7 | 8 | # Folder structure 9 | 10 |
 
11 | /src/
12 | |-- models
13 | |   `-- dogs.model
14 | |-- business
15 | |   `-- dogs.business
16 | |-- controllers
17 | |   `-- dogs.controller
18 | |-- routes
19 | |   `-- dogs.route
20 | |-- sockets
21 | |   `-- dogs.socket
22 | |-- seeds
23 | |   `-- dogs.seed
24 | |-- cronjobs
25 | |   `-- dogs.cronjob
26 | |-- utils
27 | |   `-- helper.util
28 | |-- layouts
29 | |   `-- login.layout
30 | |-- libs
31 | |   `-- express.lib
32 | `-- app
33 | 
34 | -------------------------------------------------------------------------------- /docs/getting-started/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Getting Started 3 | has_children: true 4 | nav_order: 2 5 | --- 6 | 7 | # Getting Started 8 | -------------------------------------------------------------------------------- /docs/getting-started/installation.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Installation 3 | parent: Getting Started 4 | has_children: false 5 | nav_order: 1 6 | --- 7 | 8 | # Installation 9 | 10 | ## Requirements 11 | 12 | - [Nodejs](https://nodejs.org) latest (minimum version 6.x.x) 13 | - [MongoDB](https://www.mongodb.com) latest (minimum version 3.x.x) 14 | - [Redis](https://redis.io) >= latest (minimum version 3.x.x) 15 | 16 | ## Clone repository 17 | 18 | ```bash 19 | git clone https://github.com/kevoj/nodetomic 20 | cd nodetomic 21 | yarn 22 | ``` 23 | 24 | Then, you will need to create a **.env** file in the root of the project, you can check the [guide here](https://kevoj.github.io/nodetomic/environments) 25 | 26 | ### Example .env file: 27 | 28 | ```bash 29 | PROJECT_MODE=development 30 | PROJECT_NAME=example-name 31 | SERVER_HOSTNAME=localhost 32 | SERVER_PORT=8000 33 | SERVER_WEBSOCKET_PORT=8001 34 | SWAGGER_HOSTNAME=localhost 35 | SWAGGER_API_DOCS=true 36 | JWT_SECRET_KEY=shhhh 37 | MONGODB_HOSTNAME=127.0.0.1 38 | MONGODB_PORT=27017 39 | MONGODB_DATABASE=example-dev 40 | MONGODB_USERNAME= 41 | MONGODB_PASSWORD= 42 | REDIS_HOSTNAME=127.0.0.1 43 | REDIS_PORT=6379 44 | REDIS_PASSWORD= 45 | ``` 46 | 47 | Once the .env is configured, you can start the project 48 | 49 | ```bash 50 | yarn start 51 | ``` 52 | 53 | ![image](https://user-images.githubusercontent.com/2652129/128099115-68acdd08-22f4-41c8-b2f2-35d320db9a14.png) 54 | 55 | To see more scripts you can check the [guide here](https://kevoj.github.io/nodetomic/scripts) -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Welcome 3 | has_children: false 4 | nav_order: 1 5 | --- 6 | 7 | # NODETOMIC 8 | 9 | Minimalist boilerplate for **nodejs**, designed for vertical and horizontal scalability. 10 | 11 | ## Technologies 12 | 13 | - Express 14 | - Redis 6 15 | - MongoDB 4 16 | - Swagger 3 17 | - Webpack 5 18 | - Babel 7 19 | - Socket 4 20 | - Eslint 21 | - Prettier 22 | - Jest 23 | 24 | ## Scalability 25 | 26 | ### Starting point 27 | 28 | ![nodetomic_1](https://user-images.githubusercontent.com/2652129/128117943-ba569149-8f3c-4252-9231-9e16936167a2.png) 29 | 30 | ### cluster mode **(NO SHARED STATE)** 31 | 32 | ![nodetomic_2](https://user-images.githubusercontent.com/2652129/128117945-cd4abb81-7c36-4cc3-8de8-0f8b809c6988.png) 33 | 34 | ### Add Redis to shared state 35 | 36 | ![nodetomic_3](https://user-images.githubusercontent.com/2652129/128117950-b576e53a-d14b-4b7c-96cc-c317958c1bd3.png) 37 | 38 | ### Added multiple servers and pm2 load balancing 39 | 40 | ![nodetomic_4](https://user-images.githubusercontent.com/2652129/128117954-be4c1813-5222-474c-bac1-40ffd6aace60.png) 41 | 42 | ### Added database and load balancer 43 | 44 | ![nodetomic_5](https://user-images.githubusercontent.com/2652129/128117959-e2893fb2-7588-4fb0-8625-b237be20dad2.png) 45 | 46 | ### Added redis cluster and mongodb sharded clusters 47 | 48 | ![nodetomic_6](https://user-images.githubusercontent.com/2652129/128117966-7bbc6054-97a7-4ae4-bfc1-71071c41fdd7.png) 49 | 50 | ### Conclusion 51 | 52 | ![nodetomic_7](https://user-images.githubusercontent.com/2652129/128117968-de8d3d3f-25af-4b5f-bfab-cac9d9e9dac9.png) 53 | 54 | ## API docs 55 | 56 | ![image](https://user-images.githubusercontent.com/2652129/128109277-2a7bed2d-f6e7-4fe8-8e67-215fbf60f186.png) 57 | 58 | ## Installation 59 | 60 | [Guide](https://kevoj.github.io/nodetomic/getting-started/installation.html) 61 | -------------------------------------------------------------------------------- /docs/scripts/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Scripts 3 | has_children: false 4 | nav_order: 8 5 | --- 6 | 7 | # Scripts 8 | 9 | ### start 10 | 11 | Start the project in development mode with the .env file that is in the root 12 | 13 | ```bash 14 | yarn start 15 | ``` 16 | 17 | ### test 18 | 19 | Run the unit tests hosted on **tests** 20 | 21 | ```bash 22 | yarn test 23 | ``` 24 | 25 | ### build 26 | 27 | Compile the project with the .env file that is in the root, its output is in dist/app.js 28 | 29 | ```bash 30 | yarn build 31 | ``` 32 | -------------------------------------------------------------------------------- /docs/unit-tests/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Unit tests 3 | has_children: false 4 | nav_order: 6 5 | --- 6 | 7 | # Unit tests 8 | 9 | To create unit tests, you must create a file with the extension **test.js** inside __test__ folder. 10 | 11 | __test_/dogs.test.js 12 | 13 | ```javascript 14 | // Libs 15 | import { create } from 'apisauce'; 16 | 17 | const api = create({ 18 | baseURL: host 19 | }); 20 | 21 | describe('Dogs', () => { 22 | test('all dogs', async () => { 23 | const { status, data } = await api.get('/api/dogs/all'); 24 | expect(200).toBe(status); 25 | expect([]).toEqual(data); 26 | }); 27 | 28 | test('all dogs - logged', async () => { 29 | const { status, data } = await api.get('/api/dogs/all/logged'); 30 | expect(401).toBe(status); 31 | expect('unauthorized').toEqual(data?.result); 32 | }); 33 | }); 34 | ``` 35 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES6", 4 | "module": "commonjs", 5 | "allowSyntheticDefaultImports": true, 6 | "baseUrl": ".", 7 | "paths": { 8 | "@/*": ["src/*"] 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "nodetomic", 4 | "version": "1.0.0", 5 | "description": "Node, Express, MongoDB, Sockets, Redis, JWT, Webpack, Babel 7", 6 | "main": "src/index.js", 7 | "scripts": { 8 | "start": "webpack --watch --mode=development", 9 | "build": "webpack --mode=production", 10 | "test": "jest", 11 | "serve": "yarn build && node dist/app.js" 12 | }, 13 | "keywords": [ 14 | "nodetomic", 15 | "nodejs", 16 | "boilerplate", 17 | "express", 18 | "redis", 19 | "mongodb", 20 | "node-boilerplate", 21 | "express-boilerplate" 22 | ], 23 | "repository": { 24 | "type": "git", 25 | "url": "https://github.com/kevoj/nodetomic.git" 26 | }, 27 | "author": { 28 | "name": "leonardo Rico", 29 | "email": "leonardo.ricog@gmail.com", 30 | "url": "https://github.com/kevoj" 31 | }, 32 | "license": "MIT", 33 | "dependencies": { 34 | "apisauce": "^2.1.1", 35 | "bcrypt": "^5.0.1", 36 | "body-parser": "^1.19.0", 37 | "chalk": "^4.1.2", 38 | "compression": "^1.7.4", 39 | "cookie-parser": "^1.4.5", 40 | "cors": "^2.8.5", 41 | "crypto": "^1.0.1", 42 | "dotenv": "^10.0.0", 43 | "express": "^4.17.1", 44 | "express-easy-helper": "^2.5.1", 45 | "express-fileupload": "^1.2.1", 46 | "express-handlebars": "^5.3.3", 47 | "express-jsdoc-swagger": "^1.6.4", 48 | "express-rate-limit": "^5.3.0", 49 | "helmet": "^4.6.0", 50 | "ioredis": "^4.27.8", 51 | "jsonwebtoken": "^8.5.1", 52 | "method-override": "^3.0.0", 53 | "moment": "^2.29.1", 54 | "mongoose": "^5.13.7", 55 | "mongoose-aggregate-paginate-v2": "^1.0.42", 56 | "mongoose-sequence": "^5.3.1", 57 | "morgan": "^1.10.0", 58 | "request-ip": "^2.1.3", 59 | "socket.io": "^4.1.3", 60 | "socket.io-redis": "^6.1.1", 61 | "validator": "13.6.0" 62 | }, 63 | "devDependencies": { 64 | "@babel/core": "^7.15.0", 65 | "@babel/plugin-proposal-class-properties": "^7.14.5", 66 | "@babel/plugin-syntax-dynamic-import": "^7.8.3", 67 | "@babel/plugin-transform-async-to-generator": "^7.14.5", 68 | "@babel/plugin-transform-runtime": "^7.15.0", 69 | "@babel/preset-env": "^7.15.0", 70 | "@babel/register": "^7.15.3", 71 | "@babel/runtime": "^7.15.3", 72 | "babel-jest": "^27.0.6", 73 | "babel-loader": "^8.2.2", 74 | "babel-plugin-root-import": "^6.6.0", 75 | "clean-webpack-plugin": "^3.0.0", 76 | "copy-webpack-plugin": "^9.0.1", 77 | "eslint": "^7.32.0", 78 | "eslint-config-prettier": "^8.3.0", 79 | "eslint-plugin-import": "^2.24.0", 80 | "eslint-plugin-prettier": "^3.4.0", 81 | "eslint-webpack-plugin": "^3.0.1", 82 | "jest": "^27.0.6", 83 | "nodemon-webpack-plugin": "^4.5.2", 84 | "prettier": "^2.3.2", 85 | "webpack": "^5.50.0", 86 | "webpack-cli": "^4.8.0", 87 | "webpack-node-externals": "^3.0.0" 88 | }, 89 | "babel": { 90 | "presets": [ 91 | "@babel/env" 92 | ], 93 | "plugins": [ 94 | [ 95 | "babel-plugin-root-import", 96 | { 97 | "rootPathPrefix": "@/", 98 | "rootPathSuffix": "./src" 99 | } 100 | ], 101 | "@babel/plugin-transform-async-to-generator", 102 | "@babel/plugin-proposal-class-properties", 103 | "@babel/transform-runtime", 104 | "@babel/plugin-syntax-dynamic-import" 105 | ] 106 | }, 107 | "prettier": { 108 | "tabWidth": 2, 109 | "semi": true, 110 | "singleQuote": true, 111 | "trailingComma": "none", 112 | "endOfLine": "auto" 113 | }, 114 | "jest": { 115 | "verbose": true, 116 | "testEnvironment": "node", 117 | "testTimeout": 30000, 118 | "setupFilesAfterEnv": [ 119 | "/__tests__/setupTests.js" 120 | ], 121 | "testMatch": [ 122 | "/__tests__/**/*?((?!!)*.)+(test).[jt]s?(x)" 123 | ] 124 | }, 125 | "eslintConfig": { 126 | "env": { 127 | "commonjs": true, 128 | "es6": true, 129 | "node": true 130 | }, 131 | "globals": { 132 | "Atomics": "readonly", 133 | "SharedArrayBuffer": "readonly" 134 | }, 135 | "parserOptions": { 136 | "ecmaVersion": 11, 137 | "sourceType": "module" 138 | }, 139 | "extends": [ 140 | "prettier", 141 | "eslint:recommended" 142 | ], 143 | "plugins": [ 144 | "prettier" 145 | ], 146 | "rules": { 147 | "prettier/prettier": "warn", 148 | "no-var": "warn", 149 | "no-unused-vars": "warn" 150 | } 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /pm2.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "app", 3 | "script": "./dist/app.js", 4 | "instances": "max", 5 | "env": { 6 | "NODE_ENV": "development" 7 | }, 8 | "env_production": { 9 | "NODE_ENV": "production" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/app.js: -------------------------------------------------------------------------------- 1 | // base 2 | import { create as express } from '@/libs/express.lib'; 3 | import { connect as mongoose } from '@/libs/mongoose.lib'; 4 | import { connect as redis } from '@/libs/redis.lib'; 5 | import { connect as ws } from '@/libs/socketio.lib'; 6 | import autoload from '@/utils/autoload.util'; 7 | 8 | /** 9 | * init 10 | */ 11 | const init = async () => { 12 | // Connect to DB (You can enable seeds) 13 | await db(); 14 | // Connect to Redis 15 | await redis(); 16 | // Add cronjobs 17 | await cronjobs(); 18 | // Create Express app and add routes 19 | await routes(); 20 | // Connect Sockets (idle to connections...) 21 | sockets(); 22 | }; 23 | 24 | /** 25 | * db 26 | */ 27 | const db = async () => { 28 | await mongoose(); 29 | // Load Models 30 | await autoload.models(); 31 | // Load Seeds 32 | await autoload.seeds(); 33 | }; 34 | 35 | /** 36 | * cronjobs 37 | */ 38 | const cronjobs = async () => { 39 | // Load cronjobs 40 | await autoload.cronjobs(); 41 | }; 42 | 43 | /** 44 | * routes 45 | */ 46 | const routes = async () => { 47 | // Load routes 48 | await express(await autoload.routes()); 49 | }; 50 | 51 | /** 52 | * sockets 53 | */ 54 | const sockets = async () => { 55 | await ws(); 56 | }; 57 | 58 | export { init }; 59 | -------------------------------------------------------------------------------- /src/business/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bytesleo/nodetomic/2a7e4646a60e466d6bfe604cc776f9f9b12c3172/src/business/.gitkeep -------------------------------------------------------------------------------- /src/business/auth.business.js: -------------------------------------------------------------------------------- 1 | // Models 2 | import UserModel from '@/models/user.model'; 3 | 4 | /** 5 | * login 6 | * 7 | * @param {*} username 8 | * @param {*} password 9 | * @returns {object} 10 | */ 11 | const login = async (username, password) => { 12 | const user = await UserModel.findOne({ 13 | $or: [ 14 | { 15 | email: username 16 | }, 17 | { 18 | phone: username 19 | } 20 | ] 21 | }) 22 | .select('+password') 23 | .lean(); 24 | 25 | if (user) { 26 | if (user.deleted_at) 27 | throw { 28 | code: 'ERROR_LOGIN_1', 29 | message: `The user has been banned` 30 | }; 31 | if (!user.password) 32 | throw { 33 | code: 'ERROR_LOGIN_2', 34 | message: `Don't have a password, try in recover password` 35 | }; 36 | const isMatch = await UserModel.compare(password, user.password); 37 | if (!isMatch) 38 | throw { 39 | code: 'ERROR_LOGIN_3', 40 | message: `Incorrect password` 41 | }; 42 | return user; 43 | } else { 44 | throw { 45 | code: 'ERROR_LOGIN_4', 46 | message: `User not found` 47 | }; 48 | } 49 | }; 50 | 51 | /** 52 | * 53 | * register 54 | * 55 | * @param {*} username 56 | * @param {*} password 57 | * @returns {object} 58 | */ 59 | const register = async (username, password, terms) => { 60 | const code = Math.floor(1000 + Math.random() * 9000); 61 | const exists = await UserModel.exists({ 62 | $or: [ 63 | { 64 | email: username 65 | }, 66 | { 67 | phone: username 68 | } 69 | ] 70 | }); 71 | 72 | if (exists) { 73 | throw { 74 | code: 'ERROR_REGISTER_1', 75 | message: `${username} is already registered`, 76 | params: { username } 77 | }; 78 | } else { 79 | const query = {}; 80 | 81 | if (username.includes('@')) { 82 | query.email = username; 83 | query.phone = username; 84 | } else { 85 | query.phone = username; 86 | query.email = username; 87 | } 88 | 89 | const user = await UserModel.create({ 90 | ...query, 91 | password, 92 | check_terms: terms, 93 | code_verification: code 94 | }); 95 | 96 | // const page = await PagesModel.create({ user: user._id }); 97 | 98 | // Send Code 99 | if (username.includes('@')) { 100 | // sendEmail({ 101 | // to: username, 102 | // from: "hi@nodetomic.com", 103 | // subject: "Nodetomic: bienvenido", 104 | // message: `Código: ${code}`, 105 | // template: "register", 106 | // params: { 107 | // code, 108 | // }, 109 | // }); 110 | } else { 111 | // sendSMS({ 112 | // to: username, 113 | // from: "Nodetomic", 114 | // message: `Nodetomic: ${code}`, 115 | // }); 116 | } 117 | 118 | // relate to other collections 119 | // user.page = page 120 | // const created = await user.save() 121 | 122 | return user; 123 | } 124 | }; 125 | 126 | /** 127 | * recover 128 | * 129 | * @param {*} email 130 | * @returns {object} 131 | */ 132 | const recover = async (username) => { 133 | const code = Math.floor(1000 + Math.random() * 9000); 134 | 135 | const user = await UserModel.findOne({ 136 | $or: [ 137 | { 138 | email: username 139 | }, 140 | { 141 | phone: username 142 | } 143 | ], 144 | deleted_at: null 145 | }).lean(); 146 | 147 | if (user) { 148 | // Send code here via Email 149 | await UserModel.updateOne({ _id: user._id }, { code_verification: code }); 150 | 151 | if (username.includes('@')) { 152 | // sendEmail({ 153 | // to: username, 154 | // from: "hi@nodetomic.com", 155 | // subject: "Nodetomic: recuperar cuenta", 156 | // message: `Nodetomic: ${code}`, 157 | // template: "recover", 158 | // params: { 159 | // code, 160 | // }, 161 | // }); 162 | } else { 163 | // sendSMS({ 164 | // to: username, 165 | // from: "Nodetomic", 166 | // message: `Nodetomic: ${code}`, 167 | // }); 168 | } 169 | 170 | return { 171 | sent: `Sent code to ${username}` 172 | }; 173 | } else { 174 | throw { 175 | code: 'ERROR_RECOVER_1', 176 | message: `${username} is not registered`, 177 | params: { username } 178 | }; 179 | } 180 | }; 181 | 182 | /** 183 | * me 184 | * 185 | * @param {*} userId 186 | * @returns {object} 187 | */ 188 | const me = async (user_id) => { 189 | return await UserModel.findOne({ _id: user_id, deleted_at: null }) 190 | .select('phone email name last_name created_at') 191 | .lean(); 192 | }; 193 | 194 | /** 195 | * verify 196 | * 197 | * @param {*} username 198 | * @param {*} code 199 | * @returns {object} 200 | */ 201 | const verify = async (username, code) => { 202 | const user = await UserModel.findOne({ 203 | $or: [ 204 | { 205 | email: username 206 | }, 207 | { 208 | phone: username 209 | } 210 | ], 211 | code_verification: code, 212 | deleted_at: null 213 | }).lean(); 214 | 215 | if (user) { 216 | return await UserModel.findOneAndUpdate( 217 | { _id: user._id }, 218 | { code_verification: null }, 219 | { new: true } 220 | ); 221 | } else { 222 | throw { 223 | code: 'ERROR_VERIFY_1', 224 | message: `Invalid code`, 225 | params: { code } 226 | }; 227 | } 228 | }; 229 | 230 | export default { 231 | login, 232 | register, 233 | recover, 234 | me, 235 | verify 236 | }; 237 | -------------------------------------------------------------------------------- /src/business/dogs.business.js: -------------------------------------------------------------------------------- 1 | // Models 2 | import DogsModel from '@/models/dogs.model'; 3 | 4 | const getAll = async () => { 5 | // Database query 6 | return await DogsModel.find({}); 7 | }; 8 | 9 | const getAllLogged = async (user_id) => { 10 | // Database query 11 | return await DogsModel.find({ user_id }); 12 | }; 13 | 14 | export default { 15 | getAll, 16 | getAllLogged 17 | }; 18 | -------------------------------------------------------------------------------- /src/constants/config.constant.js: -------------------------------------------------------------------------------- 1 | // Env 2 | import dotenv from 'dotenv'; 3 | dotenv.config(); 4 | 5 | export const PROJECT_MODE = process.env.PROJECT_MODE; 6 | 7 | export const PROJECT_NAME = process.env.PROJECT_NAME; 8 | 9 | export const SERVER_HOSTNAME = process.env.SERVER_HOSTNAME; 10 | 11 | export const SERVER_PORT = process.env.SERVER_PORT; 12 | 13 | export const SERVER_WEBSOCKET_PORT = process.env.SERVER_WEBSOCKET_PORT; 14 | 15 | export const SWAGGER_HOSTNAME = process.env.SWAGGER_HOSTNAME; 16 | 17 | export const SWAGGER_API_DOCS = process.env.SWAGGER_API_DOCS; 18 | 19 | export const JWT_SECRET_KEY = process.env.JWT_SECRET_KEY; 20 | 21 | export const MONGODB_HOSTNAME = process.env.MONGODB_HOSTNAME; 22 | 23 | export const MONGODB_PORT = process.env.MONGODB_PORT; 24 | 25 | export const MONGODB_DATABASE = process.env.MONGODB_DATABASE; 26 | 27 | export const MONGODB_USERNAME = process.env.MONGODB_USERNAME; 28 | 29 | export const MONGODB_PASSWORD = process.env.MONGODB_PASSWORD; 30 | 31 | export const REDIS_HOSTNAME = process.env.REDIS_HOSTNAME; 32 | 33 | export const REDIS_PORT = process.env.REDIS_PORT; 34 | 35 | export const REDIS_PASSWORD = process.env.REDIS_PASSWORD; 36 | 37 | // (seconds, by default trimester) 38 | export const REDIS_TTL = { 39 | day: 86400, 40 | week: 604800, 41 | month: 2592000, 42 | bimester: 5184000, 43 | trimester: 7776000, 44 | semester: 15552000, 45 | year: 31104000 46 | }; 47 | -------------------------------------------------------------------------------- /src/controllers/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bytesleo/nodetomic/2a7e4646a60e466d6bfe604cc776f9f9b12c3172/src/controllers/.gitkeep -------------------------------------------------------------------------------- /src/controllers/auth.controller.js: -------------------------------------------------------------------------------- 1 | import validator from 'validator'; 2 | // Business 3 | import AuthBusiness from '@/business/auth.business'; 4 | // Utils 5 | import { session } from '@/utils/auth.util'; 6 | import { success, error, unauthorized } from '@/utils/helper.util'; 7 | 8 | /** 9 | * login 10 | * 11 | * @param {*} req 12 | * @param {*} res 13 | * @returns 14 | */ 15 | const login = async (req, res) => { 16 | try { 17 | const { username, password } = req.body; 18 | 19 | if (validator.isEmpty(username)) { 20 | throw { 21 | code: 'ERROR_AUTH_1', 22 | message: 'The username cannot be empty' 23 | }; 24 | } 25 | 26 | if (validator.isEmpty(password)) { 27 | throw { 28 | code: 'ERROR_AUTH_2', 29 | message: 'The password cannot be empty' 30 | }; 31 | } 32 | 33 | const user = await AuthBusiness.login(username, password); 34 | if (user) { 35 | const { _id, permissions } = user; 36 | const token = await session(_id, { permissions }); 37 | return success(res, { token }); 38 | } else { 39 | return unauthorized(res); 40 | } 41 | } catch (err) { 42 | error(res, err); 43 | } 44 | }; 45 | 46 | /** 47 | * register 48 | * 49 | * @param {*} req 50 | * @param {*} res 51 | * @returns 52 | */ 53 | const register = async (req, res) => { 54 | try { 55 | const { username, password } = req.body; 56 | 57 | if (validator.isEmpty(username)) { 58 | throw { 59 | code: 'ERROR_AUTH_1', 60 | message: 'The username cannot be empty' 61 | }; 62 | } 63 | 64 | if (validator.isEmpty(password)) { 65 | throw { 66 | code: 'ERROR_AUTH_2', 67 | message: 'The password cannot be empty' 68 | }; 69 | } 70 | 71 | const data = await AuthBusiness.register(username, password); 72 | let created = '_id' in data || 'n' in data; 73 | return success(res, 201, { created }); 74 | } catch (err) { 75 | error(res, err); 76 | } 77 | }; 78 | 79 | /** 80 | * recover 81 | * 82 | * @param {*} req 83 | * @param {*} res 84 | * @returns 85 | */ 86 | const recover = async (req, res) => { 87 | try { 88 | const { username } = req.body; 89 | 90 | if (validator.isEmpty(username)) { 91 | throw { 92 | code: 'ERROR_AUTH_1', 93 | message: 'The username cannot be empty' 94 | }; 95 | } 96 | 97 | const data = await AuthBusiness.recover(username); 98 | return success(res, data); 99 | } catch (err) { 100 | error(res, err); 101 | } 102 | }; 103 | 104 | /** 105 | * me 106 | * 107 | * @param {*} req 108 | * @param {*} res 109 | * @returns 110 | */ 111 | const me = async (req, res) => { 112 | try { 113 | const user_id = req.user.id; 114 | 115 | if (validator.isEmpty(user_id)) { 116 | throw { 117 | code: 'ERROR_AUTH_3', 118 | message: 'The User id cannot be empty' 119 | }; 120 | } 121 | 122 | if (!validator.isMongoId(user_id)) { 123 | throw { 124 | code: 'ERROR_AUTH_4', 125 | message: 'Invalid auth User id...' 126 | }; 127 | } 128 | 129 | if (user_id) { 130 | let data = await AuthBusiness.me(user_id); 131 | return data ? success(res, data) : unauthorized(res); 132 | } else { 133 | return unauthorized(res); 134 | } 135 | } catch (err) { 136 | error(res, err); 137 | } 138 | }; 139 | 140 | /** 141 | * verify 142 | * 143 | * @param {*} req 144 | * @param {*} res 145 | * @returns 146 | */ 147 | const verify = async (req, res) => { 148 | try { 149 | const { username, code } = req.body; 150 | 151 | if (validator.isEmpty(username)) { 152 | throw { 153 | code: 'ERROR_AUTH_1', 154 | message: 'The username cannot be empty' 155 | }; 156 | } 157 | 158 | if (validator.isEmpty(code)) { 159 | throw { 160 | code: 'ERROR_AUTH_5', 161 | message: 'The code cannot be empty' 162 | }; 163 | } 164 | 165 | const user = await AuthBusiness.verify(username, code); 166 | if (user) { 167 | const { _id, permissions } = user; 168 | const token = await session(_id, { permissions }); 169 | return success(res, { token }); 170 | } else { 171 | return unauthorized(res); 172 | } 173 | } catch (err) { 174 | error(res, err); 175 | } 176 | }; 177 | 178 | export default { login, register, recover, me, verify }; 179 | -------------------------------------------------------------------------------- /src/controllers/dogs.controller.js: -------------------------------------------------------------------------------- 1 | // Business 2 | import DogBusiness from '@/business/dogs.business'; 3 | import { success, error } from '@/utils/helper.util'; 4 | // Libs 5 | import validator from 'validator'; 6 | 7 | const getAll = async (req, res) => { 8 | try { 9 | // Business logic 10 | const data = await DogBusiness.getAll(); 11 | // Return success 12 | success(res, data); 13 | } catch (err) { 14 | // Return error (if any) 15 | error(res, err); 16 | } 17 | }; 18 | 19 | const getAllLogged = async (req, res) => { 20 | try { 21 | // Get current user id from session 22 | const user_id = req.user.id; 23 | // Validate data format 24 | if (!validator.isMongoId(user_id)) { 25 | throw { 26 | code: 'ERROR_AUTH_4', 27 | message: 'Invalid auth User id...' 28 | }; 29 | } 30 | // Business logic 31 | const data = await DogBusiness.getAllLogged(user_id); 32 | // Return success 33 | success(res, data); 34 | } catch (err) { 35 | // Return error (if any) 36 | error(res, err); 37 | } 38 | }; 39 | 40 | export default { getAll, getAllLogged }; 41 | -------------------------------------------------------------------------------- /src/cronjobs/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bytesleo/nodetomic/2a7e4646a60e466d6bfe604cc776f9f9b12c3172/src/cronjobs/.gitkeep -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | // Constants 3 | import { 4 | PROJECT_MODE, 5 | SERVER_HOSTNAME, 6 | SERVER_PORT, 7 | SERVER_WEBSOCKET_PORT 8 | } from '@/constants/config.constant'; 9 | // App 10 | import { init } from '@/app'; 11 | import { app } from '@/libs/express.lib'; 12 | 13 | (async () => { 14 | try { 15 | await init(); 16 | app.listen(SERVER_PORT, () => { 17 | console.log( 18 | `-------\n${chalk.black.bgGreenBright( 19 | `🚀 Server is ready!` 20 | )}\nmode: ${chalk.blueBright( 21 | `${PROJECT_MODE}` 22 | )}\nserver: ${chalk.blueBright( 23 | `http://${SERVER_HOSTNAME}:${SERVER_PORT}` 24 | )}\nsocket: ${chalk.blueBright( 25 | `http://${SERVER_HOSTNAME}:${SERVER_WEBSOCKET_PORT}` 26 | )}\n-------` 27 | ); 28 | }); 29 | } catch (error) { 30 | console.log(`${chalk.red.bold(error)}`); 31 | } 32 | })(); 33 | -------------------------------------------------------------------------------- /src/layouts/email.recover.hbs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Recuperar contraseña 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 |

Recuperar contraseña

14 |

¿Has olvidado tu contraseña? No te preocupes, genera una nueva contraseña.

15 |

Tu código de recuperión

16 |

{{code}}

17 | www.example.com 18 |
19 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /src/layouts/email.register.hbs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Bienvenido a Nodetomic 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 |

¡Bienvenido!

14 |

Hola Mundo

15 |

Tu código de verificación

16 |

{{code}}

17 | www.example.com 18 |
19 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /src/layouts/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bytesleo/nodetomic/2a7e4646a60e466d6bfe604cc776f9f9b12c3172/src/layouts/favicon.png -------------------------------------------------------------------------------- /src/layouts/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: sans-serif; 4 | } 5 | div { 6 | display: flex; 7 | justify-content: center; 8 | align-items: center; 9 | flex-direction: column; 10 | height: 100vh; 11 | } 12 | img { 13 | max-width: 180px; 14 | } 15 | h1, 16 | span { 17 | font-weight: 100; 18 | } 19 | h1 { 20 | color: #333; 21 | margin: 8px; 22 | } 23 | span { 24 | color: #6f6f6f; 25 | } 26 | a { 27 | color: #00bcd4; 28 | margin: 8px; 29 | font-weight: 600; 30 | } 31 | -------------------------------------------------------------------------------- /src/layouts/index.hbs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {{name}} 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 | 15 |

{{name}}

16 | {{mode}} 17 | {{#if docs}} 18 | api-docs 19 | {{/if}} 20 |
21 | 22 | 23 | -------------------------------------------------------------------------------- /src/layouts/logo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bytesleo/nodetomic/2a7e4646a60e466d6bfe604cc776f9f9b12c3172/src/layouts/logo.gif -------------------------------------------------------------------------------- /src/libs/express.lib.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import express from 'express'; 3 | import handlebars from 'express-handlebars'; 4 | import bodyParser from 'body-parser'; 5 | import methodOverride from 'method-override'; 6 | import rateLimit from 'express-rate-limit'; 7 | import cookieParser from 'cookie-parser'; 8 | import compression from 'compression'; 9 | import helmet from 'helmet'; 10 | import cors from 'cors'; 11 | import morgan from 'morgan'; 12 | import fileUpload from 'express-fileupload'; 13 | import requestIp from 'request-ip'; 14 | // Constants 15 | import { PROJECT_NAME, PROJECT_MODE } from '@/constants/config.constant'; 16 | // Libs 17 | import { create as swagger } from '@/libs/swagger.lib'; 18 | 19 | const app = express(); 20 | 21 | const create = async (routes) => { 22 | // parse body params and attache them to req.body 23 | app.use( 24 | bodyParser.json({ 25 | limit: '25mb', 26 | extended: true 27 | }) 28 | ); 29 | app.use(bodyParser.urlencoded({ extended: true })); 30 | 31 | // gzip compression 32 | app.use(compression()); 33 | 34 | // lets you use HTTP verbs such as PUT or DELETE 35 | // in places where the client doesn't support it 36 | app.use(methodOverride()); 37 | 38 | // enable rate limit 39 | app.use( 40 | rateLimit({ 41 | windowMs: 1 * 60 * 1000, // 1 minutes 42 | max: 1000, // limit each IP to 1000 requests per windowMs 43 | message: 'You have exceeded the requests in 24 hrs limit!', 44 | headers: true 45 | }) 46 | ); 47 | app.use(cookieParser()); 48 | 49 | // secure apps by setting various HTTP headers 50 | app.use( 51 | helmet({ 52 | contentSecurityPolicy: false 53 | }) 54 | ); 55 | 56 | // enable CORS - Cross Origin Resource Sharing 57 | app.use(cors()); 58 | 59 | // file upload 60 | app.use(fileUpload()); 61 | 62 | // client ip 63 | app.use(requestIp.mw()); 64 | 65 | // logs 66 | app.use(morgan('dev')); 67 | 68 | // Routes 69 | if (routes.length > 0) app.use(routes); 70 | 71 | // swagger 72 | const swaggerConfig = await swagger(app); 73 | 74 | // handlebars 75 | app.engine('.hbs', handlebars({ extname: '.hbs', defaultLayout: false })); 76 | app.set('view engine', '.hbs'); 77 | // views 78 | app.set('views', path.resolve(__dirname, './../src/layouts')); 79 | // index 80 | app.get('/', (_, res) => 81 | res.render('index', { 82 | name: PROJECT_NAME, 83 | mode: PROJECT_MODE, 84 | docs: swaggerConfig.exposeSwaggerUI ? swaggerConfig.swaggerUIPath : false 85 | }) 86 | ); 87 | // Static 88 | app.use(express.static(path.resolve(__dirname, './../src/layouts'))); 89 | }; 90 | 91 | export { create, app }; 92 | -------------------------------------------------------------------------------- /src/libs/mongoose.lib.js: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | // Constants 3 | import { 4 | MONGODB_HOSTNAME, 5 | MONGODB_PORT, 6 | MONGODB_DATABASE, 7 | MONGODB_USERNAME, 8 | MONGODB_PASSWORD, 9 | PROJECT_MODE 10 | } from '@/constants/config.constant'; 11 | 12 | const PORT = require('net').isIP(MONGODB_HOSTNAME) ? `:${MONGODB_PORT}` : ''; 13 | 14 | // print mongoose logs in dev env 15 | if (PROJECT_MODE === 'development') { 16 | // mongoose.set('debug', true); 17 | } 18 | 19 | const connect = () => 20 | new Promise((resolve, reject) => { 21 | mongoose.connect( 22 | MONGODB_USERNAME && MONGODB_PASSWORD 23 | ? `mongodb://${MONGODB_USERNAME}:${MONGODB_PASSWORD}@${MONGODB_HOSTNAME}${PORT}/${MONGODB_DATABASE}` 24 | : `mongodb://${MONGODB_HOSTNAME}${PORT}/${MONGODB_DATABASE}`, 25 | { 26 | useCreateIndex: true, 27 | useNewUrlParser: true, 28 | useFindAndModify: false, 29 | useUnifiedTopology: true 30 | } 31 | ); 32 | const db = mongoose.connection; 33 | 34 | db.once('connected', () => { 35 | console.log('✅ MongoDB: connected!'); 36 | resolve(); 37 | }); 38 | 39 | db.on('error', (error) => { 40 | console.error('❌ MongoDB: error'); 41 | reject(error); 42 | }); 43 | }); 44 | 45 | export { connect }; 46 | -------------------------------------------------------------------------------- /src/libs/redis.lib.js: -------------------------------------------------------------------------------- 1 | import Redis from 'ioredis'; 2 | // Constants 3 | import { 4 | REDIS_HOSTNAME, 5 | REDIS_PORT, 6 | REDIS_PASSWORD 7 | } from '@/constants/config.constant'; 8 | 9 | let redis; 10 | const PORT = require('net').isIP(REDIS_HOSTNAME) ? `:${REDIS_PORT}` : ''; 11 | 12 | const connect = () => 13 | new Promise((resolve, reject) => { 14 | const r = new Redis( 15 | REDIS_PASSWORD 16 | ? `redis://:${REDIS_PASSWORD}@${REDIS_HOSTNAME}${PORT}/0` 17 | : `redis://${REDIS_HOSTNAME}${PORT}/0` 18 | ); 19 | 20 | r.on('connect', function () { 21 | console.log('✅ Redis: connected!'); 22 | redis = r; 23 | resolve(); 24 | }); 25 | 26 | r.on('error', (err) => { 27 | console.error('❌ Redis: error'); 28 | reject(err); 29 | }); 30 | }); 31 | 32 | export { connect, redis }; 33 | -------------------------------------------------------------------------------- /src/libs/socketio.lib.js: -------------------------------------------------------------------------------- 1 | import redisAdapter from 'socket.io-redis'; 2 | // Constants 3 | import { 4 | SERVER_WEBSOCKET_PORT, 5 | REDIS_HOSTNAME, 6 | REDIS_PORT, 7 | REDIS_PASSWORD 8 | } from '@/constants/config.constant'; 9 | // Business 10 | // import UserBusiness from '@/business/users.business'; 11 | // Utils 12 | import autoload from '@/utils/autoload.util'; 13 | import { mws } from '@/utils/middleware.util'; 14 | 15 | const io = require('socket.io')(SERVER_WEBSOCKET_PORT); 16 | const PORT = require('net').isIP(REDIS_HOSTNAME) ? `:${REDIS_PORT}` : ''; 17 | 18 | io.adapter( 19 | redisAdapter( 20 | REDIS_PASSWORD 21 | ? `redis://:${REDIS_PASSWORD}@${REDIS_HOSTNAME}${PORT}/1` 22 | : `redis://${REDIS_HOSTNAME}${PORT}/1` 23 | ) 24 | ); 25 | // io.eio.pingTimeout = 120000; // 2 minutes 26 | // io.eio.pingInterval = 5000; // 5 seconds 27 | 28 | const connect = () => 29 | new Promise((resolve) => { 30 | console.log(`✅ Socket: initiated!`); 31 | // connection 32 | io.on('connection', (socket) => { 33 | console.log(`❕Socket: client connected! (${socket.id})`); 34 | 35 | // disconnect 36 | socket.on('disconnect', (reason) => { 37 | console.log(`❕Socket: client disconnected! (${socket.id}) ${reason}`); 38 | // UserBusiness.removeSocket(socket); 39 | }); 40 | // socket.set("pingTimeout", 63000); 41 | // autoload 42 | autoload.sockets(socket, io); 43 | resolve(); 44 | }); 45 | 46 | // middleware 47 | io.use(async (socket, next) => { 48 | socket.onAny(async (event) => { 49 | console.log(`Socket: event: ${event} (${socket.id})`); 50 | }); 51 | 52 | const _next = await mws(socket, next); 53 | // UserBusiness.setSocket(socket); 54 | return _next; 55 | }); 56 | }); 57 | 58 | const connections = () => io.engine.clientsCount; 59 | 60 | export { connect, connections }; 61 | -------------------------------------------------------------------------------- /src/libs/swagger.lib.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import swagger from 'express-jsdoc-swagger'; 3 | // Constants 4 | import { 5 | PROJECT_NAME, 6 | SERVER_HOSTNAME, 7 | SERVER_PORT, 8 | SWAGGER_HOSTNAME, 9 | SWAGGER_API_DOCS 10 | } from '@/constants/config.constant'; 11 | 12 | const create = async (app) => { 13 | const servers = SWAGGER_HOSTNAME.split(','); 14 | 15 | const options = { 16 | info: { 17 | title: PROJECT_NAME, 18 | description: 'API description', 19 | version: '1.0.0' 20 | }, 21 | servers: servers.map((x) => ({ 22 | url: 23 | x.includes('localhost') || x === '' 24 | ? `http://${SERVER_HOSTNAME}:${SERVER_PORT}` 25 | : x, 26 | description: '' 27 | })), 28 | security: { 29 | JWT: { 30 | type: 'apiKey', 31 | in: 'header', 32 | name: 'Authorization', 33 | description: '' 34 | } 35 | }, 36 | filesPattern: ['routes/**/[!!]*.js', 'utils/**/[!!]*.js'], // Glob pattern to find your jsdoc files (it supports arrays too ['./**/*.controller.js', './**/*.route.js']) 37 | swaggerUIPath: '/api-docs', // SwaggerUI will be render in this url. Default: '/api-docs' 38 | baseDir: path.resolve(__dirname, './../src'), 39 | exposeSwaggerUI: SWAGGER_API_DOCS === 'true' ? true : false, // Expose OpenAPI UI. Default true 40 | exposeApiDocs: false, // Expose Open API JSON Docs documentation in `apiDocsPath` path. Default false. 41 | apiDocsPath: '/api-docs' // Open API JSON Docs endpoint. Default value '/v3/api-docs'. 42 | }; 43 | 44 | swagger(app)(options); 45 | 46 | return options; 47 | }; 48 | 49 | export { create }; 50 | -------------------------------------------------------------------------------- /src/models/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bytesleo/nodetomic/2a7e4646a60e466d6bfe604cc776f9f9b12c3172/src/models/.gitkeep -------------------------------------------------------------------------------- /src/models/dogs.model.js: -------------------------------------------------------------------------------- 1 | import { Schema, model } from 'mongoose'; 2 | 3 | // Schema 4 | const schema = new Schema({ 5 | name: { 6 | type: String, 7 | default: null 8 | }, 9 | race: { 10 | type: String, 11 | default: null 12 | }, 13 | user_id: { 14 | type: Schema.Types.ObjectId, 15 | default: null 16 | }, 17 | created_at: { 18 | type: Date, 19 | default: Date.now 20 | } 21 | }); 22 | 23 | const Model = model('Dog', schema); 24 | 25 | export default Model; 26 | -------------------------------------------------------------------------------- /src/models/user.model.js: -------------------------------------------------------------------------------- 1 | import mongoose, { Schema, model } from 'mongoose'; 2 | import aggregatePaginate from 'mongoose-aggregate-paginate-v2'; 3 | import AutoIncrement from 'mongoose-sequence'; 4 | import bcrypt from 'bcrypt'; 5 | 6 | // Schema 7 | const schema = new Schema({ 8 | id: { 9 | type: Number, 10 | default: null 11 | }, 12 | phone: { 13 | type: String, 14 | trim: true, 15 | uppercase: true, 16 | index: { unique: true } 17 | }, 18 | email: { 19 | type: String, 20 | trim: true, 21 | uppercase: true, 22 | index: { unique: true } 23 | }, 24 | name: { 25 | type: String, 26 | uppercase: true, 27 | trim: true, 28 | default: null 29 | }, 30 | last_name: { 31 | type: String, 32 | uppercase: true, 33 | trim: true, 34 | default: null 35 | }, 36 | password: { 37 | type: String, 38 | select: false, 39 | default: null 40 | }, 41 | permissions: { 42 | type: Array, 43 | default: ['user'] 44 | }, 45 | code_verification: { 46 | type: String, 47 | default: null 48 | }, 49 | created_at: { 50 | type: Date, 51 | default: Date.now 52 | }, 53 | updated_at: { 54 | type: Date, 55 | default: Date.now 56 | }, 57 | deleted_at: { 58 | type: Date, 59 | default: null 60 | } 61 | }); 62 | 63 | // Plugins 64 | schema.plugin(aggregatePaginate); 65 | schema.plugin(AutoIncrement(mongoose), { id: 'user_seq', inc_field: 'id' }); 66 | 67 | // Statics 68 | schema.statics.compare = async (candidatePassword, password) => { 69 | return await bcrypt.compareSync(candidatePassword, password); 70 | }; 71 | 72 | // Hooks 73 | schema.pre('save', async function () { 74 | const user = this; 75 | if (user.password) { 76 | const hash = await bcrypt.hashSync(user.password, 10); 77 | user.password = hash; 78 | } 79 | }); 80 | 81 | schema.pre('updateOne', async function () { 82 | const user = this._update; 83 | if (user.password) { 84 | const hash = await bcrypt.hashSync(user.password, 10); 85 | this._update.password = hash; 86 | } 87 | }); 88 | 89 | schema.pre('updateMany', async function () { 90 | const user = this._update; 91 | if (user.password) { 92 | const hash = await bcrypt.hashSync(user.password, 10); 93 | this._update.password = hash; 94 | } 95 | }); 96 | 97 | // Indexes 98 | schema.index({ id: 1 }); 99 | 100 | const Model = model('User', schema); 101 | 102 | export default Model; 103 | -------------------------------------------------------------------------------- /src/routes/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bytesleo/nodetomic/2a7e4646a60e466d6bfe604cc776f9f9b12c3172/src/routes/.gitkeep -------------------------------------------------------------------------------- /src/routes/auth.route.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | // Controllers 3 | import AuthController from '@/controllers/auth.controller'; 4 | // Utils 5 | import { mw } from '@/utils/middleware.util'; 6 | // Constants 7 | const router = express.Router(); 8 | 9 | /** 10 | * POST /api/auth/login 11 | * @summary Login user 12 | * @tags Auth 13 | * @param {string} request.body.required - email or phone 14 | * @return {object} 200 - Success 15 | * @return {object} 5XX - Error 16 | * @example request - example payload 17 | * { 18 | * "username":"user@example.com", 19 | * "password":"123" 20 | * } 21 | * @example response - 200 - success response example 22 | * { 23 | * "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiI2MDFiNGJmYTU1N2QwODRkMmM3YzhiOTk6c3IySnZWTTkiLCJwZXJtaXNzaW9ucyI6WyJhZG1pbiJdLCJpYXQiOjE2MTI0MDE2ODV9.CfjPZupSqOx7DyayHnlqqy5n5TqAtk4AmOkmRITpFZk" 24 | * } 25 | * @example response - 5XX - ERROR_AUTH_1 26 | * { 27 | * "err": { 28 | * "code": "ERROR_AUTH_1", 29 | * "message": "The username cannot be empty" 30 | * } 31 | * } 32 | * @example response - 5XX - ERROR_AUTH_2 33 | * { 34 | * "err": { 35 | * "code": "ERROR_AUTH_2", 36 | * "message": "The password cannot be empty" 37 | * } 38 | * } 39 | * @example response - 5XX - ERROR_LOGIN_1 40 | * { 41 | * "err": { 42 | * "code": "ERROR_LOGIN_1", 43 | * "message": "The user has been banned" 44 | * } 45 | * } 46 | * @example response - 5XX - ERROR_LOGIN_2 47 | * { 48 | * "err": { 49 | * "code": "ERROR_LOGIN_2", 50 | * "message": "Don't have a password, try in recover password" 51 | * } 52 | * } 53 | * @example response - 5XX - ERROR_LOGIN_3 54 | * { 55 | * "err": { 56 | * "code": "ERROR_LOGIN_3", 57 | * "message": "Incorrect password" 58 | * } 59 | * } 60 | * @example response - 5XX - ERROR_LOGIN_4 61 | * { 62 | * "err": { 63 | * "code": "ERROR_LOGIN_4", 64 | * "message": "User not found" 65 | * } 66 | * } 67 | */ 68 | router.post('/api/auth/login', AuthController.login); 69 | 70 | /** 71 | * POST /api/auth/register 72 | * @summary Register user 73 | * @tags Auth 74 | * @param {string} username.form.required - email or phone 75 | * @param {string} password.form.required - user's password 76 | * @return {object} 200 - Success 77 | * @return {object} 5XX - Error 78 | * @example request - example payload 79 | * { 80 | * "username":"user@app.com", 81 | * "password":"123" 82 | * } 83 | * @example response - 200 - success response example 84 | * { 85 | * "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiI1ZmQ0MGFlMjdmNTc4OTI3M2NkMzhlODQ6bzRWYzFUTHAiLCJwZXJtaXNzaW9ucyI6WyJ1c2VyIl0sImlhdCI6MTYxMjU3NjgxNH0._tu9Ycl_WbhFzJZ2_tugJPCfnTUUQs84-eKiElr6Z6o" 86 | * } 87 | * @example response - 5XX - ERROR_AUTH_1 88 | * { 89 | * "err": { 90 | * "code": "ERROR_AUTH_1", 91 | * "message": "The username cannot be empty" 92 | * } 93 | * } 94 | * @example response - 5XX - ERROR_AUTH_2 95 | * { 96 | * "err": { 97 | * "code": "ERROR_AUTH_2", 98 | * "message": "The password cannot be empty" 99 | * } 100 | * } 101 | * @example response - 5XX - ERROR_REGISTER_1 102 | * { 103 | * "err": { 104 | * "code": "ERROR_REGISTER_1", 105 | * "message": "user@app.com is already registered", 106 | * "params": { 107 | * "username": "user@app.com" 108 | * } 109 | * } 110 | * } 111 | */ 112 | router.post('/api/auth/register', AuthController.register); 113 | 114 | /** 115 | * POST /api/auth/recover 116 | * @summary Recover password 117 | * @tags Auth 118 | * @param {string} username.form.required - email or phone 119 | * @return {object} 200 - Success 120 | * @return {object} 5XX - Error 121 | * @example request - example payload 122 | * { 123 | * "username":"user@app.com" 124 | * } 125 | * @example response - 200 - success response example 126 | * { 127 | * "sent": "Sent code to user@app.com" 128 | * } 129 | * @example response - 5XX - ERROR_AUTH_1 130 | * { 131 | * "err": { 132 | * "code": "ERROR_AUTH_1", 133 | * "message": "The username cannot be empty" 134 | * } 135 | * } 136 | * @example response - 5XX - ERROR_RECOVER_1 137 | * { 138 | * "err": { 139 | * "code": "ERROR_RECOVER_1", 140 | * "message": "user@app.com is not registered", 141 | * "params": { 142 | * "username": "user@app.com" 143 | * } 144 | * } 145 | * } 146 | */ 147 | router.post('/api/auth/recover', AuthController.recover); 148 | 149 | /** 150 | * GET /api/auth/me 151 | * @summary Get info of current logged in User 152 | * @tags Auth 153 | * @security JWT 154 | * @return {object} 200 - Success 155 | * @return {object} 401 - Unauthorized 156 | * @return {object} 403 - Forbidden 157 | * @return {object} 5XX - Error 158 | */ 159 | router.get('/api/auth/me', mw(['user']), AuthController.me); 160 | 161 | /** 162 | * POST /api/auth/verify 163 | * @summary Verify account 164 | * @tags Auth 165 | * @param {string} username.form.required - email or phone 166 | * @param {string} code.form.required - code 167 | * @return {object} 200 - Success 168 | * @return {object} 5XX - Error 169 | * @example request - example payload 170 | * { 171 | * "username":"user@app.com", 172 | * "code":"7410" 173 | * } 174 | * @example response - 200 - success response example 175 | * { 176 | * "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiI2MDFiNGJmYTU1N2QwODRkMmM3YzhiOTk6MDdxSms4QVciLCJwZXJtaXNzaW9ucyI6WyJhZG1pbiJdLCJpYXQiOjE2MTI0MDY0NDZ9.BCr89sYY3pWY3TgT2MWMZO0bUCpAZpMI0tGCwlVtVvY" 177 | * } 178 | * @example response - 5XX - ERROR_AUTH_1 179 | * { 180 | * "err": { 181 | * "code": "ERROR_AUTH_1", 182 | * "message": "The username cannot be empty" 183 | * } 184 | * } 185 | * @example response - 5XX - ERROR_AUTH_5 186 | * { 187 | * "err": { 188 | * "code": "ERROR_AUTH_5", 189 | * "message": "The code cannot be empty" 190 | * } 191 | * } 192 | * @example response - 5XX - ERROR_VERIFY_1 193 | * { 194 | * "err": { 195 | * "code": "ERROR_VERIFY_1", 196 | * "message": "Invalid code", 197 | * "params": { 198 | * "code": "1234" 199 | * } 200 | * } 201 | * } 202 | */ 203 | router.post('/api/auth/verify', AuthController.verify); 204 | 205 | export default router; 206 | -------------------------------------------------------------------------------- /src/routes/dogs.route.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | // Controllers 3 | import DogsController from '@/controllers/dogs.controller'; 4 | // Utils 5 | import { mw } from '@/utils/middleware.util'; 6 | // Constants 7 | const router = express.Router(); 8 | 9 | /** 10 | * GET /api/dogs/all 11 | * @summary Get all dogs 12 | * @tags Dogs 13 | * @return {object} 200 - Success 14 | * @return {object} 5XX - Error 15 | * @example response - 200 - success response example 16 | * [ 17 | * { 18 | * "_id":"60d200765299bd36806d8999", 19 | * "name":"Sparky", 20 | * "race":"Beagle", 21 | * "user_id": "6108db02bb8ea9e69b2984a2", 22 | * "created_at":"2021-06-22T15:23:34.521Z" 23 | * }, 24 | * { 25 | * "_id":"60d200765299bd36806d899a", 26 | * "name":"Zeus", 27 | * "race":"Chihuahua", 28 | * "user_id": "6108db02bb8ea9e69b2984a2", 29 | * "created_at":"2021-06-22T15:23:34.522Z" 30 | * }, 31 | * { 32 | * "_id":"60d200765299bd36806d899b", 33 | * "name":"Poseidon", 34 | * "race":"Bulldog", 35 | * "user_id": "6108db02bb8ea9e69b2984a2", 36 | * "created_at":"2021-06-22T15:23:34.523Z" 37 | * } 38 | * ] 39 | */ 40 | router.get('/api/dogs/all', DogsController.getAll); 41 | 42 | /** 43 | * GET /api/dogs/all/logged 44 | * @summary Get all dogs (logged) 45 | * @security JWT 46 | * @tags Dogs 47 | * @return {object} 200 - Success 48 | * @return {object} 401 - Unauthorized 49 | * @return {object} 403 - Forbidden 50 | * @return {object} 5XX - Error 51 | * @example response - 200 - success response example 52 | * [ 53 | * { 54 | * "_id":"60d200765299bd36806d8999", 55 | * "name":"Sparky", 56 | * "race":"Beagle", 57 | * "user_id": "6108db02bb8ea9e69b2984a2", 58 | * "created_at":"2021-06-22T15:23:34.521Z" 59 | * }, 60 | * { 61 | * "_id":"60d200765299bd36806d899a", 62 | * "name":"Zeus", 63 | * "race":"Chihuahua", 64 | * "user_id": "6108db02bb8ea9e69b2984a2", 65 | * "created_at":"2021-06-22T15:23:34.522Z" 66 | * }, 67 | * { 68 | * "_id":"60d200765299bd36806d899b", 69 | * "name":"Poseidon", 70 | * "race":"Bulldog", 71 | * "user_id": "6108db02bb8ea9e69b2984a2", 72 | * "created_at":"2021-06-22T15:23:34.523Z" 73 | * } 74 | * ] 75 | */ 76 | router.get('/api/dogs/all/logged', mw(['user']), DogsController.getAllLogged); 77 | 78 | export default router; 79 | -------------------------------------------------------------------------------- /src/seeds/!dogs.seed.js: -------------------------------------------------------------------------------- 1 | // Models 2 | import DogsModel from '@/models/dogs.model'; 3 | // Utils 4 | import { insert } from '@/utils/seed.util'; 5 | // Data 6 | const data = [ 7 | { 8 | name: 'Sparky', 9 | race: 'Beagle', 10 | user_id: '6108db02bb8ea9e69b2984a2' 11 | }, 12 | { 13 | name: 'Zeus', 14 | race: 'Chihuahua', 15 | user_id: '6108db02bb8ea9e69b2984a2' 16 | }, 17 | { 18 | name: 'Poseidon', 19 | race: 'Bulldog', 20 | user_id: '6108db1b8d75624c1dd2ba2f' 21 | } 22 | ]; 23 | 24 | export default async () => await insert(DogsModel, data); 25 | -------------------------------------------------------------------------------- /src/seeds/!user.seed.js: -------------------------------------------------------------------------------- 1 | // Models 2 | import UserModel from '@/models/user.model'; 3 | // Utils 4 | import { insert } from '@/utils/seed.util'; 5 | // Data 6 | const data = [ 7 | { 8 | _id: '6108db02bb8ea9e69b2984a2', 9 | name: 'User', 10 | last_name: 'Example', 11 | phone: '1234567892', 12 | email: 'user@examp4le.com', 13 | password: '123', 14 | permissions: ['user'] 15 | } 16 | ]; 17 | 18 | export default async () => await insert(UserModel, data); 19 | -------------------------------------------------------------------------------- /src/seeds/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bytesleo/nodetomic/2a7e4646a60e466d6bfe604cc776f9f9b12c3172/src/seeds/.gitkeep -------------------------------------------------------------------------------- /src/sockets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bytesleo/nodetomic/2a7e4646a60e466d6bfe604cc776f9f9b12c3172/src/sockets/.gitkeep -------------------------------------------------------------------------------- /src/sockets/dogs.socket.js: -------------------------------------------------------------------------------- 1 | // Vars 2 | let socket = null; 3 | let io = null; 4 | 5 | // Constructor 6 | export default (_socket, _io) => { 7 | socket = _socket; 8 | io = _io; 9 | on(); 10 | }; 11 | 12 | // Listen events 13 | const on = () => { 14 | socket.on('dogs:ping', (data) => { 15 | io.emit('dogs:pong', data); 16 | }); 17 | }; 18 | 19 | export { socket, io }; 20 | -------------------------------------------------------------------------------- /src/utils/auth.util.js: -------------------------------------------------------------------------------- 1 | import jsonwebtoken from 'jsonwebtoken'; 2 | import moment from 'moment'; 3 | // Constants 4 | import { JWT_SECRET_KEY, REDIS_TTL } from '@/constants/config.constant'; 5 | // Utils 6 | import { redis } from '@/libs/redis.lib'; 7 | 8 | /** 9 | * hash 10 | * 11 | * @param {*} length 12 | * @returns 13 | */ 14 | const hash = (length) => { 15 | const possible = 16 | 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; 17 | let text = ''; 18 | for (let i = 0; i < length; i++) 19 | text += possible.charAt(Math.floor(Math.random() * possible.length)); 20 | return text; 21 | }; 22 | 23 | /** 24 | * sign (JWT) 25 | * 26 | * @param {*} data 27 | * @returns 28 | */ 29 | const sign = async (data) => { 30 | try { 31 | return await jsonwebtoken.sign(data, JWT_SECRET_KEY); 32 | } catch (err) { 33 | console.log({ err }); 34 | return null; 35 | } 36 | }; 37 | 38 | /** 39 | * decode (JWT) 40 | * 41 | * @param {*} token 42 | * @returns 43 | */ 44 | const decode = async (token) => { 45 | try { 46 | return await jsonwebtoken.decode(token, JWT_SECRET_KEY); 47 | } catch (err) { 48 | console.log({ err }); 49 | return null; 50 | } 51 | }; 52 | 53 | /** 54 | * session (Redis) 55 | * 56 | * @param {*} userId 57 | * @param {*} data 58 | * @returns 59 | */ 60 | const session = async (id, data) => { 61 | try { 62 | const key = `${id}:${hash(8)}`; 63 | const token = await sign({ key, ...data }); 64 | if (token) { 65 | await redis.set(key, moment().toISOString(), 'EX', REDIS_TTL.trimester); 66 | return token; 67 | } else { 68 | throw 'The key could not be created'; 69 | } 70 | } catch (err) { 71 | console.log({ err }); 72 | return null; 73 | } 74 | }; 75 | 76 | /** 77 | * check (Redis) 78 | * 79 | * @param {*} token 80 | * @returns 81 | */ 82 | const check = async (token) => { 83 | try { 84 | const decoded = await decode(token); 85 | const data = await redis.get(decoded.key); 86 | if (decoded?.key) { 87 | const [id] = decoded.key.split(':'); 88 | return decoded?.key && data ? { ...decoded, id } : null; 89 | } else { 90 | return null; 91 | } 92 | } catch (err) { 93 | console.log({ err }); 94 | return null; 95 | } 96 | }; 97 | 98 | /** 99 | * renew (Redis) 100 | * 101 | * @param {*} key 102 | */ 103 | const renew = async (key) => { 104 | try { 105 | await redis.expire(key, REDIS_TTL.trimester); 106 | } catch (err) { 107 | console.log({ err }); 108 | return null; 109 | } 110 | }; 111 | 112 | /** 113 | * destroy (Redis) 114 | * 115 | * @param {*} key 116 | */ 117 | const destroy = async (key) => { 118 | try { 119 | await redis.del(key); 120 | } catch (err) { 121 | console.log({ err }); 122 | return null; 123 | } 124 | }; 125 | 126 | export { session, check, destroy, sign, decode, hash, renew }; 127 | -------------------------------------------------------------------------------- /src/utils/autoload.util.js: -------------------------------------------------------------------------------- 1 | const routes = () => { 2 | try { 3 | const paths = require.context('../routes', true, /^((?!!).)*.js$/); 4 | return paths 5 | .keys() 6 | .map(paths) 7 | .map((x) => x.default); 8 | } catch (error) { 9 | console.error({ error }); 10 | return []; 11 | } 12 | }; 13 | 14 | const models = () => { 15 | try { 16 | const paths = require.context('../models', true, /^((?!!).)*.js$/); 17 | return paths.keys().map(paths); 18 | } catch (error) { 19 | console.error({ error }); 20 | return []; 21 | } 22 | }; 23 | 24 | const sockets = (socket, io) => { 25 | try { 26 | const paths = require.context('../sockets', true, /^((?!!).)*.js$/); 27 | return paths 28 | .keys() 29 | .map(paths) 30 | .map((x) => x.default(socket, io)); 31 | } catch (error) { 32 | console.error({ error }); 33 | return []; 34 | } 35 | }; 36 | 37 | const seeds = () => { 38 | try { 39 | const paths = require.context('../seeds', true, /^((?!!).)*.js$/); 40 | return paths 41 | .keys() 42 | .map(paths) 43 | .map((x) => x.default()); 44 | } catch (error) { 45 | console.error({ error }); 46 | return []; 47 | } 48 | }; 49 | 50 | const cronjobs = () => { 51 | try { 52 | const paths = require.context('../cronjobs', true, /^((?!!).)*.js$/); 53 | return paths 54 | .keys() 55 | .map(paths) 56 | .map((x) => x.default); 57 | } catch (error) { 58 | console.error({ error }); 59 | return []; 60 | } 61 | }; 62 | 63 | export default { models, routes, sockets, seeds, cronjobs }; 64 | -------------------------------------------------------------------------------- /src/utils/helper.util.js: -------------------------------------------------------------------------------- 1 | import { 2 | success, 3 | forbidden, 4 | unauthorized, 5 | error as errorApi 6 | } from 'express-easy-helper'; 7 | 8 | /** 9 | * error 10 | * 11 | * @param {*} res 12 | * @param {*} e 13 | * @returns 14 | */ 15 | const error = (res, e) => { 16 | console.log('\x1b[31m', { err: e }); 17 | if (e instanceof ReferenceError) { 18 | return errorApi(res, { err: 'ReferenceError' }); 19 | } else if (e instanceof TypeError) { 20 | return errorApi(res, { err: 'TypeError' }); 21 | } else { 22 | return errorApi(res, { err: e }); 23 | } 24 | }; 25 | 26 | export { success, forbidden, unauthorized, error }; 27 | -------------------------------------------------------------------------------- /src/utils/layout.util.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import handlebars from 'express-handlebars'; 3 | import { app } from '@/libs/express.lib'; 4 | 5 | const hbs = handlebars.create(); 6 | 7 | const renderTemplate = async (name, params) => { 8 | const template = await hbs.getTemplate( 9 | path.resolve(__dirname, `./../src/layouts/${name}.hbs`), 10 | { 11 | cache: app.enabled('view cache') 12 | } 13 | ); 14 | return template(params); 15 | }; 16 | 17 | export { renderTemplate }; 18 | -------------------------------------------------------------------------------- /src/utils/middleware.util.js: -------------------------------------------------------------------------------- 1 | import validator from 'validator'; 2 | // Utils 3 | import { forbidden, unauthorized, error } from '@/utils/helper.util'; 4 | import { check, renew } from '@/utils/auth.util'; 5 | 6 | /** 7 | * mw 8 | * 9 | * @param {*} required 10 | * @returns next() 11 | */ 12 | const mw = (required) => { 13 | return async (req, res, next) => { 14 | try { 15 | let token = req.headers['authorization']; 16 | 17 | if (token) { 18 | try { 19 | // Is JWT format 20 | if (!validator.isJWT(token)) throw 'Token is not valid'; 21 | 22 | // Add Bearer to authorization Header 23 | req.headers.authorization = `Bearer ${token}`; 24 | // Verify Token in Redis, if exists, then return decode token { key, ...data, iat } 25 | const decoded = await check(token); 26 | 27 | // Validate permissions 28 | if (required) { 29 | if ('permissions' in decoded) { 30 | const isAuthorized = required.filter((x) => 31 | decoded.permissions.includes(x) 32 | ); 33 | if (isAuthorized.length === 0) return forbidden(res); 34 | } 35 | } 36 | 37 | // Renew 38 | await renew(decoded.key); 39 | // Add to request 40 | req.user = decoded; 41 | 42 | return next(); 43 | } catch (errSession) { 44 | return unauthorized(res); 45 | } 46 | } else { 47 | return unauthorized(res); 48 | } 49 | } catch (err) { 50 | return error(res, err); 51 | } 52 | }; 53 | }; 54 | 55 | /** 56 | * mws 57 | * 58 | * @param {*} socket 59 | * @param {*} next 60 | * @returns next() 61 | */ 62 | const mws = async (socket, next) => { 63 | try { 64 | const token = socket.handshake.query?.Authorization; 65 | 66 | if (token) { 67 | // Is JWT format 68 | if (!validator.isJWT(token)) throw 'Token is not valid'; 69 | 70 | // Verify Token in Redis, if exists, then return decode token { key, iat } 71 | const decoded = await check(token); 72 | 73 | // Renew 74 | await renew(decoded.key); 75 | 76 | // Add to request 77 | socket.user = { 78 | ...decoded, 79 | data: JSON.parse(socket.handshake.query?.data) || null 80 | }; 81 | 82 | return next(); 83 | } else { 84 | return next(); 85 | } 86 | } catch (err) { 87 | console.log('❕Socket: error->', err.toString()); 88 | return next(err); 89 | } 90 | }; 91 | 92 | export { mw, mws }; 93 | -------------------------------------------------------------------------------- /src/utils/seed.util.js: -------------------------------------------------------------------------------- 1 | /** 2 | * insert 3 | * 4 | * @param {*} model 5 | * @param {*} data 6 | */ 7 | export const insert = async (model, data) => { 8 | try { 9 | // If you don't need mongoose hooks 10 | // await model.insertMany(data); 11 | // For being able to use mongoose hooks 12 | for (const x of data) await model.create(x); 13 | console.log('->Seed Success: ', model.collection.collectionName); 14 | } catch (error) { 15 | console.error(new Error(error)); 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /src/utils/swagger.util.js: -------------------------------------------------------------------------------- 1 | /** 2 | * GET /api/* 3 | * @summary Generic errors thrown inside controllers 4 | * @tags *Generic Errors 5 | * @security JWT 6 | * @return {object} 5XX - Error 7 | * @example response - 5XX - ERROR_PARAMS_1 8 | * { 9 | * "err": { 10 | * "code": "ERROR_PARAMS_1", 11 | * "message": "Unauthorized due to lack of required data..." 12 | * } 13 | * } 14 | * @example response - 5XX - ERROR_PARAMS_2 15 | * { 16 | * "err": { 17 | * "code": "ERROR_PARAMS_2", 18 | * "message": "Unauthorized due to data not being in expected format..." 19 | * } 20 | * } 21 | * @example response - 5XX - ERROR_AUTH_3 22 | * { 23 | * "err": { 24 | * "code": "ERROR_AUTH_3", 25 | * "message": "The User id cannot be empty" 26 | * } 27 | * } 28 | * @example response - 5XX - ERROR_AUTH_4 29 | * { 30 | * "err": { 31 | * "code": "ERROR_AUTH_4", 32 | * "message": "Invalid auth User id..." 33 | * } 34 | * } 35 | */ 36 | -------------------------------------------------------------------------------- /webpack.config.babel.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import { CleanWebpackPlugin } from 'clean-webpack-plugin'; 3 | import nodeExternals from 'webpack-node-externals'; 4 | import NodemonPlugin from 'nodemon-webpack-plugin'; 5 | import CopyPlugin from 'copy-webpack-plugin'; 6 | import ESLintPlugin from 'eslint-webpack-plugin'; 7 | 8 | const config = (env, argv) => { 9 | // const isProduction = argv.mode === "production"; 10 | return { 11 | entry: [path.resolve(__dirname, 'src')], 12 | output: { 13 | path: path.join(__dirname, 'dist'), 14 | publicPath: '/', 15 | filename: 'app.js' 16 | }, 17 | plugins: [ 18 | new CleanWebpackPlugin(), 19 | new ESLintPlugin({ 20 | extensions: ['js', 'jsx', 'ts', 'tsx'] 21 | }), 22 | new NodemonPlugin({ 23 | watch: [ 24 | path.resolve(__dirname, 'src'), 25 | path.resolve('.env'), 26 | path.resolve('package.json') 27 | ], 28 | ignore: ['./node_modules'], 29 | verbose: true, 30 | delay: '1000' 31 | }), 32 | new CopyPlugin({ patterns: [{ from: '.env' }] }) 33 | ], 34 | target: 'node', 35 | node: { 36 | // Need this when working with express, otherwise the build fails 37 | __dirname: false, // if you don't put this is, __dirname 38 | __filename: false // and __filename return blank or / 39 | }, 40 | externals: [nodeExternals()], // Need this to avoid error when working with Express 41 | module: { 42 | rules: [ 43 | { 44 | test: /\.js$/, 45 | exclude: /node_modules/, 46 | use: { 47 | loader: 'babel-loader' 48 | } 49 | } 50 | ] 51 | }, 52 | stats: { 53 | warnings: true 54 | } 55 | }; 56 | }; 57 | 58 | export default config; 59 | --------------------------------------------------------------------------------