├── .travis.yml ├── .env.example ├── src ├── services │ ├── PostService.js │ ├── MediaService.js │ ├── UserService.js │ └── AuthService.js ├── routes │ ├── auth.js │ ├── post.js │ └── media.js ├── controllers │ ├── PostController.js │ ├── AuthController.js │ └── MediaController.js └── models │ ├── Media.js │ ├── Post.js │ ├── Auth.js │ └── User.js ├── system ├── helpers │ ├── Utility.js │ ├── HttpError.js │ └── HttpResponse.js ├── routes │ └── index.js ├── controllers │ └── Controller.js └── services │ └── Service.js ├── contributing.md ├── config ├── config.js ├── server.js ├── database.js └── routes.js ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ ├── node.js.yml │ └── codeql-analysis.yml ├── index.js ├── tests └── index.js ├── LICENSE ├── package.json ├── .gitignore ├── CODE_OF_CONDUCT.md ├── .eslintrc.json └── readme.md /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 12 4 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # MongoDB Url 2 | MONGO_URL=mongodb://127.0.0.1:27017/dbName 3 | # PORT 4 | PORT=5000 5 | # Environment Name. production / development 6 | NODE_ENV=development 7 | # JWT Secret 8 | JWT_SECRET=R4ND0M5TR1NG 9 | -------------------------------------------------------------------------------- /src/services/PostService.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const { Service } = require( '../../system/services/Service' ); 3 | 4 | class PostService extends Service { 5 | constructor( model ) { 6 | super( model ); 7 | } 8 | 9 | } 10 | 11 | module.exports = { PostService }; 12 | -------------------------------------------------------------------------------- /src/services/MediaService.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const { Service } = require( '../../system/services/Service' ); 3 | 4 | class MediaService extends Service { 5 | constructor( model ) { 6 | super( model ); 7 | } 8 | 9 | } 10 | 11 | module.exports = { MediaService }; 12 | -------------------------------------------------------------------------------- /system/helpers/Utility.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | module.exports.slugify = ( text ) => { 3 | return text.toString().toLowerCase() 4 | .replace( /\s+/g, '-' ) // Replace spaces with - 5 | .replace( /[^\w\-\.]+/g, '' ) // Remove all non-word chars 6 | .replace( /\-\-+/g, '-' ) // Replace multiple - with single - 7 | .replace( /^-+/, '' ) // Trim - from start of text 8 | .replace( /-+$/, '' ); // Trim - from end of text 9 | }; 10 | -------------------------------------------------------------------------------- /src/routes/auth.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const AUthController = require( '../controllers/AuthController' ); 3 | const express = require( 'express' ), 4 | router = express.Router(); 5 | 6 | router.post( '/login', AUthController.login ); 7 | router.get( '/logout', AUthController.checkLogin, AUthController.logout ); 8 | router.post( '/register', AUthController.register ); 9 | router.post( '/changePassword', AUthController.checkLogin, AUthController.changePassword ); 10 | 11 | module.exports = router; 12 | -------------------------------------------------------------------------------- /contributing.md: -------------------------------------------------------------------------------- 1 | # Contribute 2 | ### Step 1 3 | Fork the repository (Click the Fork button at the top right of the repository) 4 | 5 | ### Step 2 6 | 7 | Create a feature branch. 8 | Example: (``git checkout -b your-new-feature``) 9 | 10 | ### Step 3 11 | 12 | Commit the changes: (``git commit -am 'Add some feature'``) (will additionally add the new changes) 13 | 14 | ### Step 4 15 | Push to the branch: (``git push origin my-new-feature``) 16 | 17 | ### Step 5 18 | 19 | Create pull request (And that's all.) 20 | -------------------------------------------------------------------------------- /src/routes/post.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const PostController = require( '../controllers/PostController' ); 3 | const express = require( 'express' ), 4 | router = express.Router(); 5 | const AuthController = require( '../controllers/AuthController' ); 6 | 7 | router.get( '/', AuthController.checkLogin, PostController.getAll ); 8 | router.get( '/:id', PostController.get ); 9 | router.post( '/', PostController.insert ); 10 | router.put( '/:id', PostController.update ); 11 | router.delete( '/:id', PostController.delete ); 12 | 13 | 14 | module.exports = router; 15 | -------------------------------------------------------------------------------- /src/routes/media.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const MediaController = require( '../controllers/MediaController' ); 3 | const express = require( 'express' ), 4 | router = express.Router(); 5 | const AuthController = require( '../controllers/AuthController' ); 6 | 7 | router.get( '/:id', AuthController.checkLogin, MediaController.get ); 8 | router.post( '/', [ AuthController.checkLogin, MediaController.upload.single( 'file' ) ], MediaController.insert ); 9 | router.delete( '/:id', AuthController.checkLogin, MediaController.delete ); 10 | 11 | 12 | module.exports = router; 13 | -------------------------------------------------------------------------------- /src/controllers/PostController.js: -------------------------------------------------------------------------------- 1 | const { Controller } = require( '../../system/controllers/Controller' ); 2 | const { PostService } = require( './../services/PostService' ); 3 | const { Post } = require( './../models/Post' ); 4 | const autoBind = require( 'auto-bind' ), 5 | postService = new PostService( 6 | new Post().getInstance() 7 | ); 8 | 9 | class PostController extends Controller { 10 | 11 | constructor( service ) { 12 | super( service ); 13 | autoBind( this ); 14 | } 15 | 16 | } 17 | 18 | module.exports = new PostController( postService ); 19 | -------------------------------------------------------------------------------- /config/config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const path = require( 'path' ); 3 | 4 | module.exports.getConfig = () => { 5 | const config = { 6 | 'MODE': 'Development', 7 | 'PORT': process.env.PORT || 5000, 8 | 'MONGO_URL': process.env.MONGO_URL, 9 | 'UPLOAD_PATH': path.resolve( `${__dirname }/../uploads` ), 10 | 'JWT_SECRET': process.env.JWT_SECRET || 'R4ND0M5TR1NG' 11 | }; 12 | 13 | // Modify for Production 14 | if ( process.env.NODE_ENV === 'production' ) { 15 | config.MODE = 'Production'; 16 | } 17 | 18 | return config; 19 | }; 20 | -------------------------------------------------------------------------------- /config/server.js: -------------------------------------------------------------------------------- 1 | const express = require( 'express' ); 2 | const bodyParser = require( 'body-parser' ); 3 | const helmet = require( 'helmet' ), 4 | server = express(); 5 | const { setRoutes } = require( './routes' ); 6 | // For security 7 | 8 | server.use( helmet() ); 9 | 10 | const cors = require( 'cors' ), 11 | // Allow Origins according to your need. 12 | corsOptions = { 13 | 'origin': '*' 14 | }; 15 | 16 | server.use( cors( corsOptions ) ); 17 | 18 | server.use( bodyParser.json() ); 19 | 20 | // Setting up Routes 21 | setRoutes( server ); 22 | 23 | module.exports = { server }; 24 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Additional context** 27 | Add any other context about the problem here. 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | require( 'dotenv' ).config(); 2 | // Initialize DB Connection 3 | require( './config/database' ); 4 | 5 | const config = require( './config/config' ).getConfig(), 6 | PORT = config.PORT; 7 | 8 | console.log( '✔ Bootstrapping Application' ); 9 | console.log( `✔ Mode: ${config.MODE}` ); 10 | console.log( `✔ Port: ${PORT}` ); 11 | 12 | const { server } = require( './config/server' ); 13 | 14 | server.listen( PORT ).on( 'error', ( err ) => { 15 | console.log( '✘ Application failed to start' ); 16 | console.error( '✘', err.message ); 17 | process.exit( 0 ); 18 | } ).on( 'listening', () => { 19 | console.log( '✔ Application Started' ); 20 | } ); 21 | 22 | 23 | module.exports = { server }; 24 | -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | branches: [ master ] 9 | pull_request: 10 | branches: [ master ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | matrix: 19 | node-version: [12.x, 14.x] 20 | 21 | steps: 22 | - uses: actions/checkout@v2 23 | - name: Use Node.js ${{ matrix.node-version }} 24 | uses: actions/setup-node@v1 25 | with: 26 | node-version: ${{ matrix.node-version }} 27 | - run: npm ci 28 | - run: npm run lint 29 | - run: npm test 30 | -------------------------------------------------------------------------------- /config/database.js: -------------------------------------------------------------------------------- 1 | const mongoose = require( 'mongoose' ), 2 | config = require( './config' ).getConfig(); 3 | 4 | // Mongo Connection Class 5 | class Connection { 6 | constructor() { 7 | const url = config.MONGO_URL; 8 | 9 | mongoose.Promise = global.Promise; 10 | mongoose.set( 'useNewUrlParser', true ); 11 | mongoose.set( 'useFindAndModify', false ); 12 | mongoose.set( 'useCreateIndex', true ); 13 | mongoose.set( 'useUnifiedTopology', true ); 14 | this.connect( url ).then( () => { 15 | console.log( '✔ Database Connected' ); 16 | } ).catch( ( err ) => { 17 | console.error( '✘ MONGODB ERROR: ', err.message ); 18 | } ); 19 | 20 | } 21 | 22 | async connect( url ) { 23 | try { 24 | await mongoose.connect( url ); 25 | } catch ( e ) { 26 | throw e; 27 | } 28 | } 29 | } 30 | 31 | module.exports = new Connection(); 32 | -------------------------------------------------------------------------------- /src/services/UserService.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const { Service } = require( '../../system/services/Service' ); 3 | const autoBind = require( 'auto-bind' ); 4 | 5 | class UserService extends Service { 6 | constructor( model ) { 7 | super( model ); 8 | this.model = model; 9 | autoBind( this ); 10 | } 11 | 12 | 13 | async updatePassword( id, data ) { 14 | try { 15 | await this.model.findByIdAndUpdate( id, data, { 'new': true } ); 16 | return { 'passwordChanged': true }; 17 | } catch ( errors ) { 18 | throw errors; 19 | } 20 | } 21 | 22 | /** 23 | * 24 | * @param email : string 25 | * @param includePassword : boolean 26 | * @returns {Promise<*>} 27 | */ 28 | async findByEmail( email, includePassword = false ) { 29 | return includePassword ? this.model.findByEmail( email ).select( '+password' ) : this.model.findByEmail( email ); 30 | } 31 | } 32 | 33 | module.exports = { UserService }; 34 | -------------------------------------------------------------------------------- /tests/index.js: -------------------------------------------------------------------------------- 1 | // Import the dependencies for testing 2 | const chai = require( 'chai' ); 3 | const chaiHttp = require( 'chai-http' ); 4 | const { server } = require( '../index' ); 5 | // Configure chai 6 | 7 | chai.use( chaiHttp ); 8 | chai.should(); 9 | describe( 'Basic Routes Testing', () => { 10 | // Test to get welcome route 11 | it( 'should return 200 status', ( done ) => { 12 | chai.request( server ) 13 | .get( '/' ) 14 | .end( ( err, res ) => { 15 | if( err ) { 16 | throw err; 17 | } 18 | res.should.have.status( 200 ); 19 | done(); 20 | } ); 21 | } ); 22 | // Test to get api base route 23 | it( 'should return 200 status', ( done ) => { 24 | chai.request( server ) 25 | .get( '/api/' ) 26 | .end( ( err, res ) => { 27 | if( err ) { 28 | throw err; 29 | } 30 | res.should.have.status( 200 ); 31 | done(); 32 | } ); 33 | } ); 34 | } ); 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Sunil Kumar Samanta 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 | -------------------------------------------------------------------------------- /config/routes.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const express = require( 'express' ); 3 | const path = require( 'path' ); 4 | const { HttpError } = require( '../system/helpers/HttpError' ); 5 | const apiRoutes = require( '../system/routes' ); 6 | 7 | module.exports.setRoutes = ( app ) => { 8 | 9 | /** 10 | * Application Root Route. 11 | * Set the Welcome message or send a static html or use a view engine. 12 | */ 13 | app.get( '/', ( req, res ) => { 14 | res.send( 'Welcome to the APP' ); 15 | } ); 16 | 17 | /** 18 | * API Route. 19 | * All the API will start with "/api/[MODULE_ROUTE]" 20 | */ 21 | app.use( '/api', apiRoutes ); 22 | 23 | /** 24 | * Serving Static files from uploads directory. 25 | * Currently Media module is uploading files into this directory. 26 | */ 27 | app.use( '/uploads', express.static( path.join( __dirname, '../uploads' ) ) ); 28 | 29 | /** 30 | * If No route matches. Send user a 404 page 31 | */ 32 | app.use( '/*', ( req, res ) => { 33 | const error = new Error( 'Requested path does not exist.' ); 34 | 35 | error.statusCode = 404; 36 | res.status( error.statusCode ).json( new HttpError( error ) ); 37 | } ); 38 | }; 39 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-mongoose-setup", 3 | "version": "1.0.0", 4 | "description": "Node Mongoose Setup", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "mocha tests/*.js --exit", 8 | "lint": "eslint .", 9 | "start": "npm run lint && node index.js" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git@github.com:sunilksamanta/node-mongoose-setup.git" 14 | }, 15 | "author": "Sunil Kumar Samanta", 16 | "license": "ISC", 17 | "dependencies": { 18 | "async": "^3.2.2", 19 | "auto-bind": "^4.0.0", 20 | "bcrypt": "^5.0.0", 21 | "body-parser": "^1.19.0", 22 | "chai": "^4.2.0", 23 | "chai-http": "^4.3.0", 24 | "cors": "^2.8.5", 25 | "crypto-random-string": "^3.2.0", 26 | "dotenv": "^8.2.0", 27 | "express": "^4.17.1", 28 | "helmet": "^4.1.1", 29 | "jsonwebtoken": "^8.5.1", 30 | "mocha": "^8.1.3", 31 | "mongoose": "^5.7.7", 32 | "mongoose-unique-validator": "^2.0.3", 33 | "multer": "^1.4.2", 34 | "pluralize": "^8.0.0", 35 | "request": "^2.88.2", 36 | "slugify": "^1.3.5" 37 | }, 38 | "devDependencies": { 39 | "eslint": "^7.12.1", 40 | "babel-eslint": "^10.1.0", 41 | "eslint-plugin-import": "^2.22.1" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/models/Media.js: -------------------------------------------------------------------------------- 1 | const mongoose = require( 'mongoose' ); 2 | const { Schema } = require( 'mongoose' ); 3 | 4 | class Media { 5 | 6 | initSchema() { 7 | const schema = new Schema( { 8 | 'originalname': { 9 | 'type': String, 10 | 'required': false, 11 | }, 12 | 'encoding': { 13 | 'type': String, 14 | 'required': false, 15 | }, 16 | 'mimetype': { 17 | 'type': String, 18 | 'required': false, 19 | }, 20 | 'filename': { 21 | 'type': String, 22 | 'required': false, 23 | }, 24 | 'path': { 25 | 'type': String, 26 | 'required': false, 27 | }, 28 | 'size': { 29 | 'type': Number, 30 | 'required': false, 31 | } 32 | }, { 'timestamps': true } ); 33 | 34 | try { 35 | mongoose.model( 'media', schema ); 36 | } catch ( e ) { 37 | 38 | } 39 | 40 | } 41 | 42 | getInstance() { 43 | this.initSchema(); 44 | return mongoose.model( 'media' ); 45 | } 46 | } 47 | 48 | module.exports = { Media }; 49 | -------------------------------------------------------------------------------- /src/models/Post.js: -------------------------------------------------------------------------------- 1 | const mongoose = require( 'mongoose' ); 2 | const { Schema } = require( 'mongoose' ); 3 | const uniqueValidator = require( 'mongoose-unique-validator' ); 4 | const slugify = require( 'slugify' ); 5 | 6 | class Post { 7 | 8 | initSchema() { 9 | const schema = new Schema( { 10 | 'title': { 11 | 'type': String, 12 | 'required': true, 13 | }, 14 | 'slug': String, 15 | 'subtitle': { 16 | 'type': String, 17 | 'required': false, 18 | }, 19 | 'description': { 20 | 'type': String, 21 | 'required': false, 22 | }, 23 | 'content': { 24 | 'type': String, 25 | 'required': true, 26 | } 27 | }, { 'timestamps': true } ); 28 | 29 | schema.pre( 'save', function( next ) { 30 | const post = this; 31 | 32 | if ( !post.isModified( 'title' ) ) { 33 | return next(); 34 | } 35 | post.slug = slugify( post.title, '_' ); 36 | console.log( 'set slug', post.slug ); 37 | return next(); 38 | } ); 39 | schema.plugin( uniqueValidator ); 40 | try { 41 | mongoose.model( 'post', schema ); 42 | } catch ( e ) { 43 | 44 | } 45 | 46 | } 47 | 48 | getInstance() { 49 | this.initSchema(); 50 | return mongoose.model( 'post' ); 51 | } 52 | } 53 | 54 | module.exports = { Post }; 55 | -------------------------------------------------------------------------------- /system/helpers/HttpError.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | class HttpError { 4 | error = true; 5 | responseTimestamp = new Date(); 6 | 7 | /** 8 | * HTTP Error Class 9 | * @author Sunil Kumar Samanta 10 | * @param error 11 | */ 12 | constructor( error ) { 13 | if ( typeof ( error ) === 'string' ) { 14 | this.statusCode = 500; 15 | this.message = error; 16 | this.name = 'InternalServerError'; 17 | } else { 18 | if ( error.name === 'ValidationError' ) { 19 | error.statusCode = 422; 20 | } 21 | 22 | let errorName = 'InternalServerError'; 23 | 24 | switch ( error.statusCode ) { 25 | case 422: 26 | errorName = 'ValidationError'; 27 | break; 28 | case 401: 29 | errorName = 'UnauthorizedError'; 30 | break; 31 | case 403: 32 | errorName = 'ForbiddenError'; 33 | break; 34 | case 404: 35 | errorName = 'NotFoundError'; 36 | break; 37 | default: 38 | errorName = 'InternalServerError'; 39 | } 40 | this.statusCode = error.statusCode ? error.statusCode : 500; 41 | this.message = error.message || 'Something wrong!'; 42 | this.errors = error.errors; 43 | this.name = errorName; 44 | } 45 | } 46 | } 47 | 48 | module.exports = { HttpError }; 49 | -------------------------------------------------------------------------------- /system/routes/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const express = require( 'express' ); 3 | const router = express.Router(); 4 | const pluralize = require( 'pluralize' ); 5 | const path = require( 'path' ); 6 | 7 | pluralize.addUncountableRule( 'media' ); 8 | pluralize.addUncountableRule( 'auth' ); 9 | 10 | const fs = require( 'fs' ); 11 | const { HttpError } = require( '../helpers/HttpError' ); 12 | const packageJson = require( '../../package.json' ), 13 | routesPath = path.resolve( `${__dirname}/../../src/routes` ), 14 | PATHS = fs.readdirSync( routesPath ), 15 | moduleMapper = []; 16 | 17 | console.log( '✔ Mapping routes' ); 18 | PATHS.forEach( ( module ) => { 19 | if( module !== 'index.js' ) { 20 | const name = module.split( '.' )[ 0 ]; 21 | 22 | // eslint-disable-next-line global-require 23 | router.use( `/${pluralize.plural( name )}`, require( path.resolve( routesPath, module ) ) ); 24 | moduleMapper.push( { 25 | 'Module': name, 26 | 'Route': `/${pluralize.plural( name )}` 27 | } ); 28 | } 29 | } ); 30 | 31 | 32 | console.table( moduleMapper ); 33 | 34 | router.get( '/', ( req, res ) => { 35 | res.json( { 'status': true, 'message': `Welcome to ${packageJson.name} V ${packageJson.version}` } ); 36 | } ); 37 | 38 | router.use( '*', ( req, res, next ) => { 39 | // 404 handler 40 | const error = new Error( 'Resource not found' ); 41 | 42 | error.statusCode = 404; 43 | next( error ); 44 | } ); 45 | 46 | router.use( ( err, req, res, next ) => { 47 | if( process.env.NODE_ENV !== 'production' ) { 48 | console.error( req.method, req.url, err.statusCode, err.message ); 49 | } 50 | const error = new HttpError( err ); 51 | 52 | res.status( error.statusCode ); 53 | res.json( error ); 54 | next(); 55 | } ); 56 | module.exports = router; 57 | -------------------------------------------------------------------------------- /src/models/Auth.js: -------------------------------------------------------------------------------- 1 | const mongoose = require( 'mongoose' ); 2 | const { Schema } = require( 'mongoose' ); 3 | const jwt = require( 'jsonwebtoken' ), 4 | config = require( '../../config/config' ).getConfig(), 5 | jwtKey = config.JWT_SECRET, 6 | jwtExpirySeconds = 172800; 7 | 8 | class Auth { 9 | 10 | initSchema() { 11 | const schema = new Schema( { 12 | 'token': { 13 | 'type': String, 14 | 'required': true, 15 | }, 16 | 'user': { 17 | 'type': Schema.Types.ObjectId, 18 | 'required': true, 19 | 'ref': 'user' 20 | } 21 | }, { 'timestamps': true } ); 22 | 23 | schema.statics.generateToken = async function( user ) { 24 | // Create a new token with the user details 25 | try { 26 | const token = await jwt.sign( { 27 | '_id': user._id.toString(), 28 | 'email': user.email, 29 | 'name': user.name, 30 | 'role': user.role 31 | }, jwtKey, { 32 | 'algorithm': 'HS256', 33 | 'expiresIn': jwtExpirySeconds, 34 | } ); 35 | 36 | return token; 37 | } catch ( e ) { 38 | throw e; 39 | } 40 | }; 41 | 42 | schema.statics.decodeToken = async function( token ) { 43 | // Create a new token with the user details 44 | try { 45 | return await jwt.verify( token, jwtKey ); 46 | } catch ( e ) { 47 | throw e; 48 | } 49 | }; 50 | try { 51 | mongoose.model( 'auth', schema ); 52 | } catch ( e ) { 53 | 54 | } 55 | 56 | } 57 | 58 | getInstance() { 59 | this.initSchema(); 60 | return mongoose.model( 'auth' ); 61 | } 62 | } 63 | 64 | module.exports = { Auth }; 65 | -------------------------------------------------------------------------------- /system/helpers/HttpResponse.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const defaultExcludedItemsFromResponse = [ '__v', 'password' ]; 3 | 4 | class HttpResponse { 5 | 6 | /** 7 | * @author Sunil Kumar Samanta 8 | * @param data : Object | Array | String 9 | * @param options : {totalCount: Number, statusCode: Number, deleted: Boolean} 10 | */ 11 | error = false; 12 | responseTimestamp = new Date(); 13 | 14 | constructor( data, options = { 'totalCount': 0, 'statusCode': 200, 'deleted': null } ) { 15 | this.statusCode = options.statusCode || 200; 16 | let filteredData = data; 17 | 18 | if ( typeof ( filteredData ) === 'object' ) { 19 | filteredData = this.filterData( JSON.parse( JSON.stringify( filteredData ) ) ); 20 | } 21 | if ( options.deleted ) { 22 | this.deleted = options.deleted; 23 | } 24 | if ( Array.isArray( filteredData ) ) { 25 | this.data = [ ...filteredData ]; 26 | this.totalCount = options.totalCount || undefined; 27 | } else if ( typeof ( filteredData ) === 'object' ) { 28 | this.data = { ...filteredData }; 29 | } else { 30 | this.data = data; 31 | } 32 | } 33 | 34 | filterData( data ) { 35 | if ( Array.isArray( data ) ) { 36 | data.map( ( x, index ) => { 37 | Object.keys( x ).forEach( ( key ) => { 38 | if ( defaultExcludedItemsFromResponse.includes( key ) ) { 39 | delete data[ index ][ key ]; 40 | } 41 | } ); 42 | } ); 43 | } else if ( typeof ( data ) === 'object' ) { 44 | Object.keys( data ).forEach( ( key ) => { 45 | if ( defaultExcludedItemsFromResponse.includes( key ) ) { 46 | delete data[ key ]; 47 | } 48 | } ); 49 | } 50 | return data; 51 | } 52 | } 53 | 54 | module.exports = { HttpResponse }; 55 | -------------------------------------------------------------------------------- /system/controllers/Controller.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const autoBind = require( 'auto-bind' ); 3 | 4 | class Controller { 5 | 6 | /** 7 | * Base Controller Layer 8 | * @author Sunil Kumar Samanta 9 | * @param service 10 | */ 11 | constructor( service ) { 12 | this.service = service; 13 | autoBind( this ); 14 | } 15 | 16 | async getAll( req, res, next ) { 17 | try { 18 | const response = await this.service.getAll( req.query ); 19 | 20 | return res.status( response.statusCode ).json( response ); 21 | } catch ( e ) { 22 | next( e ); 23 | } 24 | } 25 | 26 | async get( req, res, next ) { 27 | const { id } = req.params; 28 | 29 | try { 30 | const response = await this.service.get( id ); 31 | 32 | return res.status( response.statusCode ).json( response ); 33 | } catch ( e ) { 34 | next( e ); 35 | } 36 | } 37 | 38 | async insert( req, res, next ) { 39 | try { 40 | const response = await this.service.insert( req.body ); 41 | 42 | return res.status( response.statusCode ).json( response ); 43 | } catch ( e ) { 44 | next( e ); 45 | } 46 | } 47 | 48 | async update( req, res, next ) { 49 | const { id } = req.params; 50 | 51 | try { 52 | const response = await this.service.update( id, req.body ); 53 | 54 | return res.status( response.statusCode ).json( response ); 55 | } catch ( e ) { 56 | next( e ); 57 | } 58 | } 59 | 60 | async delete( req, res, next ) { 61 | const { id } = req.params; 62 | 63 | try { 64 | const response = await this.service.delete( id ); 65 | 66 | return res.status( response.statusCode ).json( response ); 67 | } catch ( e ) { 68 | next( e ); 69 | } 70 | } 71 | 72 | } 73 | 74 | module.exports = { Controller }; 75 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | name: "CodeQL" 7 | 8 | on: 9 | push: 10 | branches: [master] 11 | pull_request: 12 | # The branches below must be a subset of the branches above 13 | branches: [master] 14 | schedule: 15 | - cron: '0 17 * * 2' 16 | 17 | jobs: 18 | analyze: 19 | name: Analyze 20 | runs-on: ubuntu-latest 21 | 22 | strategy: 23 | fail-fast: false 24 | matrix: 25 | # Override automatic language detection by changing the below list 26 | # Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python'] 27 | language: ['javascript'] 28 | # Learn more... 29 | # https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection 30 | 31 | steps: 32 | - name: Checkout repository 33 | uses: actions/checkout@v2 34 | 35 | # Initializes the CodeQL tools for scanning. 36 | - name: Initialize CodeQL 37 | uses: github/codeql-action/init@v1 38 | with: 39 | languages: ${{ matrix.language }} 40 | # If you wish to specify custom queries, you can do so here or in a config file. 41 | # By default, queries listed here will override any specified in a config file. 42 | # Prefix the list here with "+" to use these queries and those in the config file. 43 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 44 | 45 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 46 | # If this step fails, then you should remove it and run the build manually (see below) 47 | - name: Autobuild 48 | uses: github/codeql-action/autobuild@v1 49 | 50 | # ℹ️ Command-line programs to run using the OS shell. 51 | # 📚 https://git.io/JvXDl 52 | 53 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 54 | # and modify them (or add more) to build your code if your project 55 | # uses a compiled language 56 | 57 | #- run: | 58 | # make bootstrap 59 | # make release 60 | 61 | - name: Perform CodeQL Analysis 62 | uses: github/codeql-action/analyze@v1 63 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Node template 3 | # Logs 4 | logs 5 | *.log 6 | npm-debug.log* 7 | yarn-debug.log* 8 | yarn-error.log* 9 | lerna-debug.log* 10 | 11 | # Diagnostic reports (https://nodejs.org/api/report.html) 12 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 13 | 14 | # Runtime data 15 | pids 16 | *.pid 17 | *.seed 18 | *.pid.lock 19 | 20 | # Directory for instrumented libs generated by jscoverage/JSCover 21 | lib-cov 22 | 23 | # Coverage directory used by tools like istanbul 24 | coverage 25 | *.lcov 26 | 27 | # nyc test coverage 28 | .nyc_output 29 | 30 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 31 | .grunt 32 | 33 | # Bower dependency directory (https://bower.io/) 34 | bower_components 35 | 36 | # node-waf configuration 37 | .lock-wscript 38 | 39 | # Compiled binary addons (https://nodejs.org/api/addons.html) 40 | build/Release 41 | 42 | # Dependency directories 43 | node_modules/ 44 | jspm_packages/ 45 | 46 | # Snowpack dependency directory (https://snowpack.dev/) 47 | web_modules/ 48 | 49 | # TypeScript cache 50 | *.tsbuildinfo 51 | 52 | # Optional npm cache directory 53 | .npm 54 | 55 | # Optional eslint cache 56 | .eslintcache 57 | 58 | # Microbundle cache 59 | .rpt2_cache/ 60 | .rts2_cache_cjs/ 61 | .rts2_cache_es/ 62 | .rts2_cache_umd/ 63 | 64 | # Optional REPL history 65 | .node_repl_history 66 | 67 | # Output of 'npm pack' 68 | *.tgz 69 | 70 | # Yarn Integrity file 71 | .yarn-integrity 72 | 73 | # dotenv environment variables file 74 | .env 75 | .env.test 76 | 77 | # parcel-bundler cache (https://parceljs.org/) 78 | .cache 79 | .parcel-cache 80 | 81 | # Next.js build output 82 | .next 83 | out 84 | 85 | # Nuxt.js build / generate output 86 | .nuxt 87 | dist 88 | 89 | # Gatsby files 90 | .cache/ 91 | # Comment in the public line in if your project uses Gatsby and not Next.js 92 | # https://nextjs.org/blog/next-9-1#public-directory-support 93 | # public 94 | 95 | # vuepress build output 96 | .vuepress/dist 97 | 98 | # Serverless directories 99 | .serverless/ 100 | 101 | # FuseBox cache 102 | .fusebox/ 103 | 104 | # DynamoDB Local files 105 | .dynamodb/ 106 | 107 | # TernJS port file 108 | .tern-port 109 | 110 | # Stores VSCode versions used for testing VSCode extensions 111 | .vscode-test 112 | 113 | # yarn v2 114 | .yarn/cache 115 | .yarn/unplugged 116 | .yarn/build-state.yml 117 | .yarn/install-state.gz 118 | .pnp.* 119 | .idea 120 | uploads 121 | .vscode -------------------------------------------------------------------------------- /src/models/User.js: -------------------------------------------------------------------------------- 1 | const mongoose = require( 'mongoose' ); 2 | const { Schema } = require( 'mongoose' ); 3 | const uniqueValidator = require( 'mongoose-unique-validator' ); 4 | const bcrypt = require( 'bcrypt' ), 5 | SALT_WORK_FACTOR = 10; 6 | 7 | class User { 8 | 9 | initSchema() { 10 | const schema = new Schema( { 11 | 'name': { 12 | 'type': String, 13 | 'required': true, 14 | }, 15 | 'email': { 16 | 'type': String, 17 | 'unique': true, 18 | 'required': true, 19 | }, 20 | 'password': { 21 | 'type': String, 22 | 'required': true, 23 | 'select': false 24 | }, 25 | 'role': { 26 | 'type': String, 27 | 'enum': [ 'admin' ], 28 | 'default': 'admin' 29 | }, 30 | 'status': { 31 | 'type': Boolean, 32 | 'required': true, 33 | 'default': true 34 | } 35 | }, { 'timestamps': true } ); 36 | 37 | 38 | // Pre save Hook 39 | schema.pre( 'save', function( next ) { 40 | const user = this; 41 | // only hash the password if it has been modified (or is new) 42 | 43 | if ( this.isModified( 'password' ) || this.isNew ) { 44 | bcrypt.genSalt( SALT_WORK_FACTOR, ( err, salt ) => { 45 | if ( err ) { 46 | return next( err ); 47 | } 48 | bcrypt.hash( user.password, salt, ( hashErr, hash ) => { 49 | if ( hashErr ) { 50 | return next( hashErr ); 51 | } 52 | // override the cleartext password with the hashed one 53 | user.password = hash; 54 | next(); 55 | } ); 56 | } ); 57 | } else { 58 | return next(); 59 | } 60 | } ); 61 | 62 | // Compare Password 63 | schema.methods.comparePassword = async function( candidatePassword ) { 64 | return new Promise( ( resolve, reject ) => { 65 | bcrypt.compare( candidatePassword, this.password, ( err, isMatch ) => { 66 | if ( err ) { 67 | reject( err ); 68 | } else { 69 | resolve( isMatch ); 70 | } 71 | } ); 72 | } ); 73 | }; 74 | schema.statics.findByEmail = function( email ) { 75 | return this.findOne( { 'email': email } ); 76 | }; 77 | schema.plugin( uniqueValidator ); 78 | try { 79 | mongoose.model( 'user', schema ); 80 | } catch ( e ) { 81 | 82 | } 83 | 84 | } 85 | 86 | getInstance() { 87 | this.initSchema(); 88 | return mongoose.model( 'user' ); 89 | } 90 | } 91 | 92 | module.exports = { User }; 93 | -------------------------------------------------------------------------------- /src/controllers/AuthController.js: -------------------------------------------------------------------------------- 1 | const { AuthService } = require( './../services/AuthService' ); 2 | const { Auth } = require( './../models/Auth' ); 3 | const { User } = require( './../models/User' ); 4 | const autoBind = require( 'auto-bind' ); 5 | const bcrypt = require( 'bcrypt' ), 6 | SALT_WORK_FACTOR = 10, 7 | authService = new AuthService( 8 | new Auth().getInstance(), new User().getInstance() 9 | ); 10 | 11 | class AuthController { 12 | 13 | constructor( service ) { 14 | this.service = service; 15 | autoBind( this ); 16 | } 17 | 18 | async login( req, res, next ) { 19 | try { 20 | const response = await this.service.login( req.body.email, req.body.password ); 21 | 22 | await res.status( response.statusCode ).json( response ); 23 | } catch ( e ) { 24 | next( e ); 25 | } 26 | } 27 | 28 | async register( req, res, next ) { 29 | try { 30 | const registeredUserData = await this.service.register( req.body ); 31 | 32 | await res.status( 200 ).json( registeredUserData ); 33 | } catch ( e ) { 34 | next( e ); 35 | } 36 | } 37 | 38 | 39 | async changePassword( req, res, next ) { 40 | try { 41 | const id = req.user._id; 42 | 43 | bcrypt.genSalt( SALT_WORK_FACTOR, async( err, salt ) => { 44 | if ( err ) { 45 | return next( err ); 46 | } 47 | bcrypt.hash( req.body.password, salt, async( hashErr, hash ) => { 48 | if ( hashErr ) { 49 | return next( hashErr ); 50 | } 51 | const data = { 'password': hash }, 52 | response = await this.service.changePassword( id, data ); 53 | 54 | await res.status( response.statusCode ).json( response ); 55 | } ); 56 | } ); 57 | } catch ( e ) { 58 | next( e ); 59 | } 60 | } 61 | 62 | async logout( req, res, next ) { 63 | try { 64 | const response = await this.service.logout( req.token ); 65 | 66 | await res.status( response.statusCode ).json( response ); 67 | } catch ( e ) { 68 | next( e ); 69 | } 70 | } 71 | 72 | async checkLogin( req, res, next ) { 73 | try { 74 | const token = this.extractToken( req ); 75 | 76 | req.user = await this.service.checkLogin( token ); 77 | req.authorized = true; 78 | req.token = token; 79 | next(); 80 | } catch ( e ) { 81 | next( e ); 82 | } 83 | } 84 | 85 | extractToken( req ) { 86 | if ( req.headers.authorization && req.headers.authorization.split( ' ' )[ 0 ] === 'Bearer' ) { 87 | return req.headers.authorization.split( ' ' )[ 1 ]; 88 | } else if ( req.query && req.query.token ) { 89 | return req.query.token; 90 | } 91 | return null; 92 | } 93 | 94 | 95 | } 96 | 97 | module.exports = new AuthController( authService ); 98 | -------------------------------------------------------------------------------- /src/controllers/MediaController.js: -------------------------------------------------------------------------------- 1 | const { Controller } = require( '../../system/controllers/Controller' ); 2 | const { MediaService } = require( './../services/MediaService' ); 3 | const { Media } = require( './../models/Media' ); 4 | const autoBind = require( 'auto-bind' ); 5 | const multer = require( 'multer' ); 6 | const fs = require( 'fs' ); 7 | const utils = require( '../../system/helpers/Utility' ), 8 | config = require( '../../config/config' ).getConfig(), 9 | mediaService = new MediaService( 10 | new Media().getInstance() 11 | ); 12 | 13 | class MediaController extends Controller { 14 | 15 | // file upload using multer 16 | storage = multer.diskStorage( { 17 | 'destination': function( req, file, cb ) { 18 | const dir = config.UPLOAD_PATH; 19 | 20 | fs.exists( dir, ( exist ) => { 21 | if ( !exist ) { 22 | return fs.mkdir( dir, ( error ) => cb( error, dir ) ); 23 | } 24 | return cb( null, dir ); 25 | } ); 26 | }, 27 | 'filename': function( req, file, cb ) { 28 | const fileOriginalName = utils.slugify( file.originalname ); 29 | 30 | cb( null, `${( new Date() ).getTime() }-${ fileOriginalName}` ); 31 | } 32 | } ); 33 | upload = multer( { 34 | 'storage': this.storage, 35 | 'limits': { 36 | 'fileSize': 1024 * 1024 * 5 37 | } 38 | } ); 39 | 40 | constructor( service ) { 41 | super( service ); 42 | autoBind( this ); 43 | } 44 | 45 | async insert( req, res, next ) { 46 | try { 47 | const uploadPath = config.UPLOAD_PATH; 48 | 49 | req.file.path = req.file.path.split( `${uploadPath }/` )[ 1 ]; 50 | const response = await this.service.insert( req.file ); 51 | 52 | return res.status( response.statusCode ).json( response ); 53 | } catch ( e ) { 54 | next( e ); 55 | } 56 | } 57 | 58 | fileFilter = ( req, file, cb ) => { 59 | // reject a file 60 | if ( file.mimetype === 'image/jpeg' || file.mimetype === 'image/png' || file.mimetype === 'image/gif' ) { 61 | cb( null, true ); 62 | } else { 63 | cb( null, false ); 64 | } 65 | }; 66 | 67 | async delete( req, res, next ) { 68 | const { id } = req.params; 69 | 70 | try { 71 | const response = await this.service.delete( id ); 72 | // File Unlinking.. 73 | 74 | if ( response.data.path ) { 75 | console.log( 'unlink item', response.data.path ); 76 | fs.unlink( response.data.path, ( err ) => { 77 | if ( err ) { 78 | console.log( 'error deleting file' ); 79 | throw err; 80 | } 81 | console.log( 'File deleted!' ); 82 | } ); 83 | } 84 | return res.status( response.statusCode ).json( response ); 85 | } catch ( e ) { 86 | next( e ); 87 | } 88 | } 89 | 90 | } 91 | 92 | module.exports = new MediaController( mediaService ); 93 | -------------------------------------------------------------------------------- /system/services/Service.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const mongoose = require( 'mongoose' ); 3 | const autoBind = require( 'auto-bind' ); 4 | const { HttpResponse } = require( '../helpers/HttpResponse' ); 5 | 6 | class Service { 7 | /** 8 | * Base Service Layer 9 | * @author Sunil Kumar Samanta 10 | * @param model 11 | */ 12 | constructor( model ) { 13 | this.model = model; 14 | autoBind( this ); 15 | } 16 | 17 | async getAll( query ) { 18 | let { skip, limit, sortBy } = query; 19 | 20 | skip = skip ? Number( skip ) : 0; 21 | limit = limit ? Number( limit ) : 10; 22 | sortBy = sortBy ? sortBy : { 'createdAt': -1 }; 23 | 24 | delete query.skip; 25 | delete query.limit; 26 | delete query.sortBy; 27 | 28 | if ( query._id ) { 29 | try { 30 | query._id = new mongoose.mongo.ObjectId( query._id ); 31 | } catch ( error ) { 32 | throw new Error( 'Not able to generate mongoose id with content' ); 33 | } 34 | } 35 | 36 | try { 37 | const items = await this.model 38 | .find( query ) 39 | .sort( sortBy ) 40 | .skip( skip ) 41 | .limit( limit ), 42 | 43 | total = await this.model.countDocuments( query ); 44 | 45 | return new HttpResponse( items, { 'totalCount': total } ); 46 | } catch ( errors ) { 47 | throw errors; 48 | } 49 | } 50 | 51 | 52 | async get( id ) { 53 | try { 54 | const item = await this.model.findById( id ); 55 | 56 | if ( !item ) { 57 | const error = new Error( 'Item not found' ); 58 | 59 | error.statusCode = 404; 60 | throw error; 61 | } 62 | 63 | return new HttpResponse( item ); 64 | } catch ( errors ) { 65 | throw errors; 66 | } 67 | } 68 | 69 | async insert( data ) { 70 | try { 71 | const item = await this.model.create( data ); 72 | 73 | if ( item ) { 74 | return new HttpResponse( item ); 75 | } 76 | throw new Error( 'Something wrong happened' ); 77 | 78 | } catch ( error ) { 79 | throw error; 80 | } 81 | } 82 | 83 | async update( id, data ) { 84 | try { 85 | const item = await this.model.findByIdAndUpdate( id, data, { 'new': true } ); 86 | 87 | return new HttpResponse( item ); 88 | } catch ( errors ) { 89 | throw errors; 90 | } 91 | } 92 | 93 | async delete( id ) { 94 | try { 95 | const item = await this.model.findByIdAndDelete( id ); 96 | 97 | if ( !item ) { 98 | const error = new Error( 'Item not found' ); 99 | 100 | error.statusCode = 404; 101 | throw error; 102 | } else { 103 | return new HttpResponse( item, { 'deleted': true } ); 104 | } 105 | } catch ( errors ) { 106 | throw errors; 107 | } 108 | } 109 | } 110 | 111 | module.exports = { Service }; 112 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at sunilthedj@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /src/services/AuthService.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const { UserService } = require( './UserService' ); 3 | const autoBind = require( 'auto-bind' ); 4 | const { HttpResponse } = require( '../../system/helpers/HttpResponse' ); 5 | const mongoose = require( 'mongoose' ); 6 | 7 | class AuthService { 8 | constructor( model, userModel ) { 9 | this.model = model; 10 | this.userService = new UserService( userModel ); 11 | autoBind( this ); 12 | } 13 | 14 | /** 15 | * 16 | * @param email: String 17 | * @param password: String 18 | * @returns {Promise} 19 | */ 20 | async login( email, password ) { 21 | const user = await this.userService.findByEmail( email, true ); 22 | 23 | if ( !user ) { 24 | // User not found 25 | const error = new Error( 'Invalid Email' ); 26 | 27 | error.statusCode = 422; 28 | throw error; 29 | } else { 30 | // Process Login 31 | try { 32 | // Check Password 33 | const passwordMatched = await user.comparePassword( password ); 34 | 35 | if ( !passwordMatched ) { 36 | const error = new Error( 'Invalid Password' ); 37 | 38 | error.statusCode = 422; 39 | throw error; 40 | } 41 | const token = await this.model.generateToken( user ); 42 | 43 | await this.model.create( { token, 'user': new mongoose.mongo.ObjectId( user._id ) } ); 44 | const tokenData = await this.model.findOne( { 'token': token } ).populate( 'user' ); 45 | 46 | return new HttpResponse( tokenData ); 47 | } catch ( e ) { 48 | throw e; 49 | } 50 | 51 | } 52 | } 53 | 54 | async register( data ) { 55 | try { 56 | return await this.userService.insert( data ); 57 | } catch ( error ) { 58 | throw error; 59 | } 60 | } 61 | 62 | async changePassword( id, data ) { 63 | try { 64 | const updatedPassword = await this.userService.updatePassword( id, data ); 65 | 66 | return new HttpResponse( updatedPassword ); 67 | } catch ( error ) { 68 | throw error; 69 | } 70 | } 71 | 72 | async logout( token ) { 73 | try { 74 | await this.model.deleteOne( { token } ); 75 | return new HttpResponse( { 'logout': true } ); 76 | } catch ( error ) { 77 | throw error; 78 | } 79 | } 80 | 81 | async checkLogin( token ) { 82 | try { 83 | // Check if the token is in the Database 84 | const tokenInDB = await this.model.countDocuments( { token } ); 85 | 86 | if ( !tokenInDB ) { 87 | const error = new Error( 'Invalid Token' ); 88 | 89 | error.statusCode = 401; 90 | throw error; 91 | } 92 | // Check the token is a valid JWT 93 | const user = await this.model.decodeToken( token ); 94 | 95 | if ( !user ) { 96 | const error = new Error( 'Invalid Token' ); 97 | 98 | error.statusCode = 401; 99 | throw error; 100 | } 101 | // Check the Extracted user is active in DB 102 | const userFromDb = await this.userService.get( user._id ); 103 | 104 | if ( userFromDb.data && userFromDb.data.status ) { 105 | return userFromDb.data; 106 | } 107 | const error = new Error( 'Invalid Token' ); 108 | 109 | error.statusCode = 401; 110 | throw error; 111 | 112 | } catch ( e ) { 113 | const error = new Error( 'Invalid Token' ); 114 | 115 | error.statusCode = 401; 116 | throw error; 117 | } 118 | } 119 | 120 | } 121 | 122 | module.exports = { AuthService }; 123 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "commonjs": true, 4 | "node": true, 5 | "es6": true 6 | }, 7 | "parser": "babel-eslint", 8 | "parserOptions": { 9 | "ecmaVersion": 8 10 | }, 11 | "rules": { 12 | "semi": "error", 13 | "quotes": [2, "single", { "avoidEscape": true }], 14 | "func-style": [ 2, "expression" ], 15 | "indent": [ 2, 4, { "SwitchCase": 1 } ], 16 | "no-invalid-regexp": 2, 17 | "no-irregular-whitespace": 2, 18 | "no-negated-in-lhs": 2, 19 | "no-unreachable": 2, 20 | "use-isnan": 2, 21 | "valid-typeof": 2, 22 | "block-scoped-var": 2, 23 | "curly": 2, 24 | "default-case": 2, 25 | "no-else-return": 2, 26 | "no-eval": 2, 27 | "no-extend-native": 2, 28 | "no-extra-bind": 2, 29 | "no-fallthrough": 2, 30 | "no-floating-decimal": 2, 31 | "no-implied-eval": 2, 32 | "no-iterator": 2, 33 | "no-labels": 2, 34 | "no-lone-blocks": 2, 35 | "no-loop-func": 2, 36 | "no-multi-spaces": 2, 37 | "no-multi-str": 2, 38 | "no-native-reassign": 2, 39 | "no-new-func": 2, 40 | "no-new-wrappers": 2, 41 | "no-new": 2, 42 | "no-octal-escape": 2, 43 | "no-octal": 2, 44 | "no-param-reassign": 2, 45 | "no-proto": 2, 46 | "no-redeclare": 2, 47 | "no-return-assign": 2, 48 | "no-self-compare": 2, 49 | "no-throw-literal": 2, 50 | "no-unused-expressions": [ 2, { "allowShortCircuit": true } ], 51 | "no-useless-call": 2, 52 | "no-useless-concat": 2, 53 | "no-void": 2, 54 | "no-warning-comments": 1, 55 | "no-with": 2, 56 | "radix": [ 2, "as-needed" ], 57 | "vars-on-top": 2, 58 | "wrap-iife": [ 2, "inside" ], 59 | "yoda": 2, 60 | "no-var": 2, 61 | "prefer-const": 2, 62 | "no-catch-shadow": 2, 63 | "no-delete-var": 2, 64 | "no-shadow-restricted-names": 2, 65 | "no-shadow": 2, 66 | "no-undef-init": 2, 67 | "no-unused-vars": 2, 68 | "no-use-before-define": 2, 69 | "global-require": 2, 70 | "handle-callback-err": 1, 71 | "no-new-require": 2, 72 | "array-bracket-spacing": [ 2, "always" ], 73 | "block-spacing": [ 2, "always" ], 74 | "brace-style": [ 2, "1tbs", { "allowSingleLine": false } ], 75 | "camelcase": [ 2, { "properties": "always" } ], 76 | "comma-spacing": [ 2, { "before": false, "after": true } ], 77 | "comma-style": [ 2, "last" ], 78 | "computed-property-spacing": [ 2, "always" ], 79 | "eol-last": 2, 80 | "key-spacing": [ 2, { "beforeColon": false, "afterColon": true } ], 81 | "new-parens": 2, 82 | "newline-after-var": [ 2, "always" ], 83 | "no-array-constructor": 2, 84 | "no-bitwise": 2, 85 | "no-continue": 2, 86 | "no-lonely-if": 2, 87 | "no-mixed-spaces-and-tabs": 2, 88 | "no-multiple-empty-lines": [ 2, { "max": 2, "maxEOF": 1 } ], 89 | "no-nested-ternary": 2, 90 | "no-new-object": 2, 91 | "no-spaced-func": 2, 92 | "no-trailing-spaces": [ 2, { "skipBlankLines": true } ], 93 | "no-unneeded-ternary": 2, 94 | "object-curly-spacing": [ 2, "always" ], 95 | "operator-linebreak": [ 2, "none" ], 96 | "quote-props": [ 2, "always" ], 97 | "space-before-blocks": [ 2, "always" ], 98 | "space-before-function-paren": [ 2, "never" ], 99 | "space-in-parens": [ 2, "always" ], 100 | "space-infix-ops": 2, 101 | "space-unary-ops": [ 2, { "words": true, "nonwords": false } ], 102 | "spaced-comment": [ 2, "always" ], 103 | "arrow-parens": [ 2, "always" ], 104 | "arrow-spacing": 2, 105 | "constructor-super": 2, 106 | "generator-star-spacing": [ 2, { "before": true, "after": false } ], 107 | "no-class-assign": 2, 108 | "no-const-assign": 2, 109 | "no-dupe-class-members": 2, 110 | "no-this-before-super": 2, 111 | "prefer-arrow-callback": 1, 112 | "prefer-spread": 1, 113 | "prefer-template": 1, 114 | "require-yield": 2 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | ![REST API Setup Nodejs MongoDB](https://dev-to-uploads.s3.amazonaws.com/i/zxvx4pk4tdlqyk1x2a4c.png) 2 | 3 | ![example workflow name](https://github.com/sunilksamanta/node-mongoose-setup/workflows/Node.js%20CI/badge.svg) 4 | [![Build Status](https://travis-ci.org/sunilksamanta/node-mongoose-setup.svg?branch=master)](https://travis-ci.org/sunilksamanta/node-mongoose-setup) 5 | [![Maintainability](https://api.codeclimate.com/v1/badges/25d8ccce7230ad5eb5c3/maintainability)](https://codeclimate.com/github/sunilksamanta/node-mongoose-setup/maintainability) 6 | [![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fsunilksamanta%2Fnode-mongoose-setup.svg?type=shield)](https://app.fossa.com/projects/git%2Bgithub.com%2Fsunilksamanta%2Fnode-mongoose-setup?ref=badge_shield) 7 | [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](contributing.md) 8 | [![GitHub issues](https://img.shields.io/github/issues/sunilksamanta/node-mongoose-setup)](https://github.com/sunilksamanta/node-mongoose-setup/issues) 9 | [![GitHub stars](https://img.shields.io/github/stars/sunilksamanta/node-mongoose-setup)](https://github.com/sunilksamanta/node-mongoose-setup/stargazers) 10 | [![Twitter](https://img.shields.io/twitter/url?style=flat-square&url=https%3A%2F%2Fgithub.com%2Fsunilksamanta%2Fnode-mongoose-setup)](https://twitter.com/intent/tweet?text=Wow:&url=https%3A%2F%2Fgithub.com%2Fsunilksamanta%2Fnode-mongoose-setup) 11 | ## Installation 12 | 13 | Setup is super easy. Clone the repository - 14 | 15 | ```shell script 16 | git clone https://github.com/sunilksamanta/node-mongoose-setup 17 | cd node-mongoose-setup 18 | npm install 19 | ``` 20 | 21 | Create an ``.env`` file at the root of your project with the following. 22 | 23 | 24 | ```dotenv 25 | MONGO_URL=YOUR_MONGO_URL 26 | PORT=5000[YOUR_DESIRED_PORT] 27 | NODE_ENV=YOUR_APP_ENVIRONMENT[production/development] 28 | JWT_SECRET=YOUR_JWT_SECRET_STRING 29 | ``` 30 | 31 | An example file `.env.example` is included. 32 | 33 | Your project is ready. Now start the project. 34 | 35 | ```shell script 36 | npm start 37 | ``` 38 | 39 | Go to ``http://localhost:5000``. You should see a default welcome page. 40 | 41 | Your API base path is ``http://localhost:5000/api``. 42 | 43 | First create some accounts to get started with the authentication. 44 | 45 | ## Authentication 46 | 47 | JWT authentication is added in this project. User model is defined in models/User.js. 48 | For Register, Login, Logout use these urls — 49 | ``` 50 | [POST] api/auth/register 51 | [POST] api/auth/login 52 | [GET] api/auth/logout 53 | ``` 54 | 55 | ## Features 56 | 57 | 1. **Controller, Model & Service oriented architecture** 58 | 59 | 1. **Auth with JWT & Db Store** 60 | 61 | 1. **Async/Await support** 62 | 63 | 1. **User Module** 64 | 65 | 1. **Post Module** (Sample CRUD) 66 | 67 | 1. **Media Upload** 68 | 69 | 1. **Centralized Http Response** 70 | 71 | 1. **Error Handler** 72 | 73 | 1. **.env support** 74 | 75 | 1. **Multi Environment config setup** 76 | 77 | 1. **Autobind Methods** 78 | 79 | 1. **Built in Pagination** 80 | 81 | ## Directory Structure of the Project 82 | ``` 83 | ├─ .env 84 | ├─ .gitignore 85 | ├─ config 86 | │ ├─ config.js 87 | │ ├─ database.js 88 | │ ├─ routes.js 89 | │ └─ server.js 90 | ├─ index.js 91 | ├─ package.json 92 | ├─ system 93 | └─ src 94 | ├─ controllers 95 | │ ├─ AuthController.js 96 | │ ├─ MediaController.js 97 | │ └─ PostController.js 98 | ├─ helpers 99 | ├─ models 100 | │ ├─ Auth.js 101 | │ ├─ Media.js 102 | │ ├─ Post.js 103 | │ └─ User.js 104 | ├─ routes 105 | │ ├─ auth.js 106 | │ ├─ media.js 107 | │ └─ post.js 108 | └─ services 109 | ├─ AuthService.js 110 | ├─ MediaService.js 111 | ├─ PostService.js 112 | └─ UserService.js 113 | ``` 114 | ## Lets talk about the structure 115 | 116 | We have 2 base classes — One for Controller and another for Service. 117 | 118 | 1. **[Controller.js](system/controllers/Controller.js)** 119 | 120 | This base controller have the basic CRUD operations. To create a new controller just extend this base Controller class. 121 | 122 | 2. **[Service.js](system/services/Service.js)** 123 | 124 | This is the base Service class which includes the database operations. If you want to change the default behaviour of the services you can update this file. 125 | 126 | 127 | ## How to Create new CRUD Module? 128 | 129 | If you want to create a new Module say Post. Then you’ll have to create 4 basic files. One Controller, one Service, one Model and one route file. And add the new route in routes/index.js with desired url. 130 | There is a “Post” CRUD module included in this project for example. 131 | ``` 132 | controllers/PostController.js 133 | models/Post.js 134 | services/PostService.js 135 | routes/post.js 136 | ``` 137 | ## Overriding Base class method 138 | 139 | As an example if you see in the media Controller — the default delete method is overriden by its own class method as we have to delete the file from the file system also. So the overriden method is like bellow — 140 | ```js 141 | async delete(req, res, next) { 142 | const { id } = req.params; 143 | try { 144 | const response = await this.service.delete(id); 145 | // File Unlinking.. 146 | if (response.data.path) { 147 | console.log("unlink item", response.data.path); 148 | fs.unlink(response.data.path, function (err) { 149 | if (err) { 150 | console.log("error deleting file"); 151 | throw err; 152 | } 153 | console.log("File deleted!"); 154 | }); 155 | } 156 | return res.status(response.statusCode).json(response); 157 | } 158 | catch (e) { 159 | next(e); 160 | } 161 | } 162 | ``` 163 | 164 | ## Have questions or suggestions? 165 | You can reply to this article [REST API Structure using NodeJS MongoDB (Mongoose)](https://medium.com/@sunilksamanta/rest-api-architecture-using-nodejs-mongodb-mongoose-64f9021c906f) 166 | 167 | ## Want to contribute? 168 | 169 | If you have any suggestion, feedback or you want to make this project more powerful — feel free to report issues or request a feature or suggest some changes. 170 | 171 | Read the [Contributing guideline](contributing.md). 172 | 173 | ## License 174 | 175 | This project is licensed under the terms of the [MIT](LICENSE) license. 176 | 177 | 178 | 179 | [![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fsunilksamanta%2Fnode-mongoose-setup.svg?type=large)](https://app.fossa.com/projects/git%2Bgithub.com%2Fsunilksamanta%2Fnode-mongoose-setup?ref=badge_large) 180 | 181 | ## Credit 182 | 183 | Special thanks to [@thisk8brd](https://github.com/thisk8brd/node-starter) for the concept of this API Structure. 184 | --------------------------------------------------------------------------------